@fougere/core 0.12.2-alpha.0 → 0.12.5-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 (204) hide show
  1. package/dist/EffectiveOperationModel.d.ts +1 -4
  2. package/dist/EffectiveOperationModel.d.ts.map +1 -1
  3. package/dist/EffectiveOperationModel.js +181 -173
  4. package/dist/EffectiveOperationModel.js.map +1 -1
  5. package/dist/FougereConfig.d.ts +2 -3
  6. package/dist/FougereConfig.d.ts.map +1 -1
  7. package/dist/FougereConfig.js +11 -13
  8. package/dist/FougereConfig.js.map +1 -1
  9. package/dist/FrondDeclaration.d.ts +5 -0
  10. package/dist/FrondDeclaration.d.ts.map +1 -1
  11. package/dist/FrondDeclaration.js +4 -5
  12. package/dist/FrondDeclaration.js.map +1 -1
  13. package/dist/FrondsStated.d.ts +4 -2
  14. package/dist/FrondsStated.d.ts.map +1 -1
  15. package/dist/FrondsStated.js.map +1 -1
  16. package/dist/Source.js +1 -1
  17. package/dist/Source.js.map +1 -1
  18. package/dist/SourceView.d.ts +0 -2
  19. package/dist/SourceView.d.ts.map +1 -1
  20. package/dist/StatedFrond.d.ts +3 -0
  21. package/dist/StatedFrond.d.ts.map +1 -1
  22. package/dist/StatedFrond.js +4 -0
  23. package/dist/StatedFrond.js.map +1 -1
  24. package/dist/StatedModules.d.ts +5 -2
  25. package/dist/StatedModules.d.ts.map +1 -1
  26. package/dist/StatedModules.js +11 -6
  27. package/dist/StatedModules.js.map +1 -1
  28. package/dist/boot/App.d.ts +2 -4
  29. package/dist/boot/App.d.ts.map +1 -1
  30. package/dist/boot/AppLifecycle.d.ts +8 -0
  31. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  32. package/dist/boot/AppLifecycle.js +24 -10
  33. package/dist/boot/AppLifecycle.js.map +1 -1
  34. package/dist/boot/AuthRuntime.d.ts +5 -12
  35. package/dist/boot/AuthRuntime.d.ts.map +1 -1
  36. package/dist/boot/AuthRuntime.js +2 -1
  37. package/dist/boot/AuthRuntime.js.map +1 -1
  38. package/dist/boot/CreateAppOptions.d.ts +0 -11
  39. package/dist/boot/CreateAppOptions.d.ts.map +1 -1
  40. package/dist/boot/Emissions.d.ts.map +1 -1
  41. package/dist/boot/Emissions.js +3 -6
  42. package/dist/boot/Emissions.js.map +1 -1
  43. package/dist/boot/Extension.d.ts +6 -0
  44. package/dist/boot/Extension.d.ts.map +1 -1
  45. package/dist/boot/Hosting.d.ts +2 -1
  46. package/dist/boot/Hosting.d.ts.map +1 -1
  47. package/dist/boot/bootstrap.d.ts.map +1 -1
  48. package/dist/boot/bootstrap.js +149 -263
  49. package/dist/boot/bootstrap.js.map +1 -1
  50. package/dist/boot/install.d.ts +3 -0
  51. package/dist/boot/install.d.ts.map +1 -1
  52. package/dist/boot/install.js +103 -114
  53. package/dist/boot/install.js.map +1 -1
  54. package/dist/boot/nesting.d.ts.map +1 -1
  55. package/dist/boot/nesting.js +2 -1
  56. package/dist/boot/nesting.js.map +1 -1
  57. package/dist/boot/relations.d.ts.map +1 -1
  58. package/dist/boot/relations.js.map +1 -1
  59. package/dist/boot/remote.d.ts +2 -1
  60. package/dist/boot/remote.d.ts.map +1 -1
  61. package/dist/boot/remote.js +7 -4
  62. package/dist/boot/remote.js.map +1 -1
  63. package/dist/boot/seed.d.ts +2 -1
  64. package/dist/boot/seed.d.ts.map +1 -1
  65. package/dist/boot/seed.js +10 -29
  66. package/dist/boot/seed.js.map +1 -1
  67. package/dist/descriptor/ExtensionEntry.d.ts +4 -0
  68. package/dist/descriptor/ExtensionEntry.d.ts.map +1 -1
  69. package/dist/descriptor/ExtensionEntry.js +6 -1
  70. package/dist/descriptor/ExtensionEntry.js.map +1 -1
  71. package/dist/descriptor/FrondDescriptor.d.ts +2 -0
  72. package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
  73. package/dist/descriptor/FrondDescriptor.js +5 -0
  74. package/dist/descriptor/FrondDescriptor.js.map +1 -1
  75. package/dist/descriptor/Fronds.d.ts +5 -0
  76. package/dist/descriptor/Fronds.d.ts.map +1 -1
  77. package/dist/descriptor/Fronds.js +8 -0
  78. package/dist/descriptor/Fronds.js.map +1 -1
  79. package/dist/descriptor/index.d.ts +1 -0
  80. package/dist/descriptor/index.d.ts.map +1 -1
  81. package/dist/descriptor/index.js +1 -0
  82. package/dist/descriptor/index.js.map +1 -1
  83. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  84. package/dist/dispatch/ArgumentResolver.js +2 -1
  85. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  86. package/dist/dispatch/Facade.d.ts +3 -0
  87. package/dist/dispatch/Facade.d.ts.map +1 -1
  88. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  89. package/dist/dispatch/HandlerFacade.js +8 -4
  90. package/dist/dispatch/HandlerFacade.js.map +1 -1
  91. package/dist/dispatch/InFlight.d.ts +3 -2
  92. package/dist/dispatch/InFlight.d.ts.map +1 -1
  93. package/dist/dispatch/InFlight.js +11 -1
  94. package/dist/dispatch/InFlight.js.map +1 -1
  95. package/dist/index.d.ts +2 -4
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +2 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/node.d.ts +1 -1
  100. package/dist/node.d.ts.map +1 -1
  101. package/dist/node.js +1 -1
  102. package/dist/node.js.map +1 -1
  103. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  104. package/dist/prefab/CrudConstructor.js.map +1 -1
  105. package/dist/prefab/collector.d.ts +6 -0
  106. package/dist/prefab/collector.d.ts.map +1 -1
  107. package/dist/prefab/collector.js +26 -1
  108. package/dist/prefab/collector.js.map +1 -1
  109. package/dist/storage/Comparison.d.ts +2 -2
  110. package/dist/storage/Comparison.d.ts.map +1 -1
  111. package/dist/storage/Comparison.js +3 -1
  112. package/dist/storage/Comparison.js.map +1 -1
  113. package/dist/storage/Storage.d.ts +3 -2
  114. package/dist/storage/Storage.d.ts.map +1 -1
  115. package/dist/storage/Storage.js +23 -10
  116. package/dist/storage/Storage.js.map +1 -1
  117. package/dist/storage/Store.d.ts.map +1 -1
  118. package/dist/storage/Store.js +4 -2
  119. package/dist/storage/Store.js.map +1 -1
  120. package/dist/wire/Emit.d.ts +0 -2
  121. package/dist/wire/Emit.d.ts.map +1 -1
  122. package/dist/wire/Emit.js +1 -1
  123. package/dist/wire/Emit.js.map +1 -1
  124. package/dist/wire/Facade.d.ts +7 -0
  125. package/dist/wire/Facade.d.ts.map +1 -1
  126. package/dist/wire/Facade.js +13 -0
  127. package/dist/wire/Facade.js.map +1 -1
  128. package/dist/wire/Invocation.d.ts +2 -1
  129. package/dist/wire/Invocation.d.ts.map +1 -1
  130. package/dist/wire/Invocation.js +4 -1
  131. package/dist/wire/Invocation.js.map +1 -1
  132. package/dist/wire/InvocationContext.d.ts +2 -0
  133. package/dist/wire/InvocationContext.d.ts.map +1 -1
  134. package/dist/wire/OperationContract.d.ts +0 -4
  135. package/dist/wire/OperationContract.d.ts.map +1 -1
  136. package/dist/wire/OperationContract.js +1 -1
  137. package/dist/wire/OperationContract.js.map +1 -1
  138. package/dist/wire/OperationsMap.d.ts +0 -3
  139. package/dist/wire/OperationsMap.d.ts.map +1 -1
  140. package/dist/wire/OperationsMap.js +0 -5
  141. package/dist/wire/OperationsMap.js.map +1 -1
  142. package/dist/wire/StateShape.d.ts +27 -0
  143. package/dist/wire/StateShape.d.ts.map +1 -0
  144. package/dist/wire/StateShape.js +59 -0
  145. package/dist/wire/StateShape.js.map +1 -0
  146. package/package.json +4 -4
  147. package/src/EffectiveOperationModel.ts +221 -201
  148. package/src/FougereConfig.ts +11 -16
  149. package/src/FrondDeclaration.ts +7 -7
  150. package/src/FrondsStated.ts +4 -2
  151. package/src/Source.ts +1 -1
  152. package/src/SourceView.ts +0 -2
  153. package/src/StatedFrond.ts +5 -0
  154. package/src/StatedModules.ts +17 -9
  155. package/src/boot/App.ts +2 -4
  156. package/src/boot/AppLifecycle.ts +27 -10
  157. package/src/boot/AuthRuntime.ts +6 -13
  158. package/src/boot/CreateAppOptions.ts +0 -11
  159. package/src/boot/Emissions.ts +3 -7
  160. package/src/boot/Extension.ts +6 -0
  161. package/src/boot/Hosting.ts +2 -1
  162. package/src/boot/bootstrap.ts +179 -300
  163. package/src/boot/install.ts +124 -146
  164. package/src/boot/nesting.ts +2 -1
  165. package/src/boot/relations.ts +3 -8
  166. package/src/boot/remote.ts +8 -3
  167. package/src/boot/seed.ts +11 -38
  168. package/src/descriptor/ExtensionEntry.ts +16 -1
  169. package/src/descriptor/FrondDescriptor.ts +7 -0
  170. package/src/descriptor/Fronds.ts +10 -0
  171. package/src/descriptor/index.ts +1 -0
  172. package/src/dispatch/ArgumentResolver.ts +2 -1
  173. package/src/dispatch/Facade.ts +3 -0
  174. package/src/dispatch/HandlerFacade.ts +8 -3
  175. package/src/dispatch/InFlight.ts +16 -2
  176. package/src/index.ts +2 -4
  177. package/src/node.ts +1 -1
  178. package/src/prefab/CrudConstructor.ts +0 -11
  179. package/src/prefab/collector.ts +29 -1
  180. package/src/storage/Comparison.ts +7 -4
  181. package/src/storage/Storage.ts +26 -12
  182. package/src/storage/Store.ts +4 -2
  183. package/src/wire/Emit.ts +1 -1
  184. package/src/wire/Facade.ts +17 -10
  185. package/src/wire/Invocation.ts +3 -1
  186. package/src/wire/InvocationContext.ts +2 -0
  187. package/src/wire/OperationContract.ts +1 -1
  188. package/src/wire/OperationsMap.ts +0 -10
  189. package/src/wire/StateShape.ts +76 -0
  190. package/dist/boot/AuthConfig.d.ts +0 -14
  191. package/dist/boot/AuthConfig.d.ts.map +0 -1
  192. package/dist/boot/AuthConfig.js +0 -2
  193. package/dist/boot/AuthConfig.js.map +0 -1
  194. package/dist/boot/AuthContext.d.ts +0 -12
  195. package/dist/boot/AuthContext.d.ts.map +0 -1
  196. package/dist/boot/AuthContext.js +0 -2
  197. package/dist/boot/AuthContext.js.map +0 -1
  198. package/dist/storage/ComparisonName.d.ts +0 -3
  199. package/dist/storage/ComparisonName.d.ts.map +0 -1
  200. package/dist/storage/ComparisonName.js +0 -2
  201. package/dist/storage/ComparisonName.js.map +0 -1
  202. package/src/boot/AuthConfig.ts +0 -14
  203. package/src/boot/AuthContext.ts +0 -12
  204. package/src/storage/ComparisonName.ts +0 -3
@@ -1,5 +1,6 @@
1
1
  /** Putting one frond into the app being built: its scope, what it serves, what it takes. */
2
2
  import type { Container } from '@fougere/container';
3
+ import type { StateShape } from '../wire/StateShape.js';
3
4
  import type { ProviderEntry } from '../descriptor/ProviderEntry.js';
4
5
  import type { PresenterEntry } from '../descriptor/PresenterEntry.js';
5
6
  import { lowerFirst, type Fields, type SchemaView } from '@fougere/schema';
@@ -85,6 +86,8 @@ export interface Assembly {
85
86
  seamsOf: Map<string, Map<string, ProviderEntry[]>>;
86
87
  log: Logger;
87
88
  options: CreateAppOptions;
89
+ /** What a call's `state` may hold, read off every extension this process mounts. */
90
+ state: StateShape;
88
91
  }
89
92
 
90
93
  /**
@@ -424,6 +427,7 @@ function buildFacadeInto(
424
427
  presenter: presenterMap.get(handler.address),
425
428
  presenterScope: scope,
426
429
  middlewares: () => getMiddlewares(handler.address),
430
+ state: assembly.state,
427
431
  });
428
432
 
429
433
  // Emissions use the same contracts and execution path as direct calls, and the terms sit
@@ -457,90 +461,51 @@ function buildFacadeInto(
457
461
  ));
458
462
  }
459
463
 
460
- export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
461
- const {
462
- container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
463
- boundPorts, refused, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
464
- log, options,
465
- } = assembly;
466
-
467
- // Declared remote: keep the scanned metadata (bridges route with it),
468
- // register nothing locally — resolve() falls through to the remote façade.
469
- if (options.remotes && frond.name in options.remotes) {
470
- log.child(frond.name).info('declared remote — not hosted locally');
471
- // Its facades answer elsewhere, but what they LISTEN to was read here.
472
- for (const handler of frond.handlers) {
473
- const key = facadeKeyOf(handler.address, handler.surface);
474
- const operations = operationModel.forHandler(handler);
475
- effectiveByKey.set(key, operations);
476
- emissions.note(contractsOf(operations), key);
477
-
478
- }
479
-
480
- return;
464
+ /** Its facades answer elsewhere, but what they LISTEN to is read here, and the bridges route with it. */
465
+ function noteRemoteFrond(frond: FrondDescriptor, { operationModel, effectiveByKey, emissions, contractsOf, log }: Assembly): void {
466
+ log.child(frond.name).info('declared remote — not hosted locally');
467
+ for (const handler of frond.handlers) {
468
+ const key = facadeKeyOf(handler.address, handler.surface);
469
+ const operations = operationModel.forHandler(handler);
470
+ effectiveByKey.set(key, operations);
471
+ emissions.note(contractsOf(operations), key);
481
472
  }
482
- // A child hangs off its parent, so everything the parent registered answers here too and
483
- // nothing else has to know: `ScopeContainer.resolve` already walks up. The boot installs
484
- // parents first, which is what makes the key below resolvable.
485
- const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
486
- const scope = above.createScope();
487
- const frondLog = log.child(frond.name);
488
- // The frond's own voice, a child of the APP logger and never of `log` — which is the
489
- // boot's, so a service's line would have claimed `boot:` long after the boot was over.
490
- scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
491
-
492
- // `reads:` is what makes a cross-source reader exist here, and the list IS its
493
- // environment — a source holding none of these is never opened. Registered under
494
- // the type's own name, which is the key `depKeyOf` already derives for a plain
495
- // parameter: `constructor(private reads: Reads)` and nothing else to say.
496
- // Declaring `reads:` with nothing to build the reader is a boot that ignores a
497
- // clause: the handler asking for `Reads` then dies at its first call, on a
498
- // container message that names neither the clause nor what is missing.
499
- await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
473
+ }
500
474
 
501
- // Who owns what, and the rule that makes owning mean something. Before anything is
502
- // registered, so a bad line is named by this refusal rather than by the container's.
475
+ /**
476
+ * Who owns what, and the rule that makes owning mean something — refused before anything is
477
+ * registered, so a bad line is named here rather than by the container.
478
+ */
479
+ function ownershipOf(frond: FrondDescriptor, { entityByName, refused }: Assembly): Map<string, string> {
503
480
  sharedNames(frond, refused);
504
481
  refuseArityDrift(frond, refused);
505
482
  const owners = ownersOf(frond.providers, frond.name, refused);
506
483
  storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
507
484
  crudOnOwned(frond, owners, refused);
508
485
 
509
- const declared = registerProviders(frond, scope, options.ports, boundPorts, refused, frondLog);
510
- // An ancestor's links stand OUTSIDE this frond's own, the order `wrapping` reads — and
511
- // like a middleware, a link cannot be inherited through a container key, since a seam has
512
- // none: its realization is built rather than resolved.
513
- const seams = inheritedSeams(frond, assembly.seamsOf, declared);
514
- assembly.seamsOf.set(frond.name, seams);
486
+ return owners;
487
+ }
515
488
 
516
- // Register Storage for each entity — PascalCase type name (e.g. 'PostStorage')
517
- // When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
518
- registerStorages(frond, assembly, scope, seams, owners, frondLog);
489
+ /** A frame is read from the same `deps` every port is: asking for one IS declaring it. */
490
+ function registerFramesOf(frond: FrondDescriptor, assembly: Assembly, scope: Container, frondLog: Logger): void {
491
+ const { entityByName, frondOf, options, hosting, refused } = assembly;
492
+ const wanted = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
493
+ .flatMap((declared) => declared.deps.map((key) => ({ key, filePath: declared.filePath })));
494
+
495
+ registerFrames(scope, wanted, frond.providers, {
496
+ entityByName,
497
+ frondOf,
498
+ hostedHere: (name) => !(options.remotes && name in options.remotes),
499
+ storageFactory: options.storageFactory,
500
+ sourceOf: options.sourceOf,
501
+ transacts: options.transacts,
502
+ transacted: options.transacted,
503
+ hosting,
504
+ log: frondLog,
505
+ }, refused);
506
+ }
519
507
 
520
- // Frames, after the ORMs and before anything that may ask for one. A frame is read
521
- // from the same `deps` every other port is read from — asking for it IS declaring it,
522
- // so nothing is registered for a frame nobody wants.
523
- registerFrames(
524
- scope,
525
- [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
526
- .flatMap((d) => d.deps.map((key) => ({ key, filePath: d.filePath }))),
527
- frond.providers,
528
- {
529
- entityByName,
530
- frondOf,
531
- hostedHere: (name) => !(options.remotes && name in options.remotes),
532
- storageFactory: options.storageFactory,
533
- sourceOf: options.sourceOf,
534
- transacts: options.transacts,
535
- transacted: options.transacted,
536
- hosting: assembly.hosting,
537
- log: frondLog,
538
- },
539
- refused,
540
- );
541
-
542
- // Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
543
- const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
508
+ function registerPresenters(frond: FrondDescriptor, scope: Container, frondLog: Logger): Map<string, PresenterEntry> {
544
509
  for (const presenter of frond.presenters) {
545
510
  scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
546
511
  }
@@ -548,104 +513,117 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
548
513
  frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
549
514
  }
550
515
 
551
- // Register collectors in scope — PascalCase type name (e.g. 'UserCollector')
552
- const collectorTypeNames = new Set(frond.collectors.map((c) => c.typeName));
516
+ return new Map(frond.presenters.map((presenter) => [presenter.entityName, presenter]));
517
+ }
518
+
519
+ function registerCollectors(frond: FrondDescriptor, scope: Container, frondLog: Logger): Set<string> {
553
520
  for (const collector of frond.collectors) {
554
- const key = collectorKeyOf(collector.typeName);
555
- scope.register(key, collector.ctor, { deps: collector.deps });
521
+ scope.register(collectorKeyOf(collector.typeName), collector.ctor, { deps: collector.deps });
556
522
  }
557
523
  if (frond.collectors.length > 0) {
558
524
  frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
559
525
  }
560
526
 
561
- // Register middlewares in scope, then take them on. Resolved per call and never here:
562
- // a middleware asking for something request-scoped would otherwise be handed the one
563
- // instance the boot built — the same reason `getMiddlewares` is read at call time.
564
- registerMiddlewares(frond, scope, assembly, frondLog);
565
-
566
- // Build handler facades → registered in ROOT container (public contract)
567
- const defaultHandlers = frond.handlers.filter((h) => !h.surface);
568
- const surfaceHandlers = frond.handlers.filter((h) => h.surface);
569
- const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
527
+ return new Set(frond.collectors.map((collector) => collector.typeName));
528
+ }
570
529
 
571
- const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
572
- const buildFacade = (
573
- entity: EntityEntry | undefined,
574
- handler: HandlerEntry,
575
- targetScope: Container,
576
- facadeKey: string,
577
- ) => buildFacadeInto(building, entity, handler, targetScope, facadeKey);
578
-
579
- // A presenter is about an entity — computed fields sit on a shape — so this walks
580
- // entities. Exposing the instance lazily; the bridge resolves it on first access.
581
- exposePresenters(frond, presenterMap, container, scope);
530
+ function sayFacade(building: Building, facadeKey: string, entity: EntityEntry | undefined): void {
531
+ const operations = Object.keys(building.assembly.container.resolve(facadeKey) as object).join(', ');
532
+ building.frondLog.debug(`${facadeKey} [${operations}]`
533
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
534
+ }
582
535
 
583
- // A facade is about a handler, so this walks HANDLERS. It walked entities before,
584
- // which made an entity a precondition for being callable at all: a handler naming
585
- // none was scanned, then never built, and nothing said so.
586
- for (const handler of defaultHandlers) {
587
- // Two ways to know the subject, and the explicit one wins: `Crud(Item)` names the
588
- // entity it was built on, whatever the handler is called. Otherwise the handler's
589
- // own name is the only thing pointing at one — and pointing at nothing is legal.
590
- //
591
- // By NAME, not by identity: the scanner loads an entity through its own loader and
592
- // the handler imports it through the runtime's, so the same class arrives as two
593
- // objects. `===` compares module instances, which is not the question being asked.
536
+ /**
537
+ * A facade is about a handler, so this walks HANDLERS — pointing at no entity is legal. The
538
+ * subject is `Crud(Item)`'s when it names one, the handler's own name otherwise, compared by
539
+ * NAME: the scanner and the runtime load one class as two objects.
540
+ */
541
+ function buildDefaultFacades(building: Building, handlers: readonly HandlerEntry[]): void {
542
+ for (const handler of handlers) {
594
543
  const crudTarget = targetOf(handler.ctor);
595
544
  const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
596
- const entity = frond.entities.find((e) => e.name === subject);
545
+ const entity = building.frond.entities.find((e) => e.name === subject);
597
546
  const facadeKey = facadeKeyOf(handler.address);
598
- buildFacade(entity, handler, scope, facadeKey);
599
- frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as object).join(', ')}]`
600
- + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
547
+ buildFacadeInto(building, entity, handler, building.scope, facadeKey);
548
+ sayFacade(building, facadeKey, entity);
601
549
  }
550
+ }
602
551
 
603
- // The dual, and it stays: a shape that declares no operation answers nothing. Said
604
- // once per entity rather than deduced from a silence.
605
- for (const entity of frond.entities) {
606
- if (!defaultHandlerMap.has(entity.name)) {
607
- frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
608
- }
552
+ /** A shape that declares no operation answers nothing — said once per entity rather than deduced from a silence. */
553
+ function sayUnhandledEntities(building: Building, handlers: readonly HandlerEntry[]): void {
554
+ const handled = new Set(handlers.map((handler) => handler.address));
555
+ for (const entity of building.frond.entities) {
556
+ if (!handled.has(entity.name)) building.frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
609
557
  }
558
+ }
610
559
 
611
- // Surface handlers — create sub-scope per surface handler with scoped storage
612
- //
613
- // Pointing at nothing is legal HERE TOO. This loop used to `continue` when no entity
614
- // carried the handler's name, so `handlers/public/SearchHandler.ts` with no `Search`
615
- // entity got no facade at all and no line saying why — while the very same handler at
616
- // the default surface is built and logged. One rule, both surfaces.
617
- for (const handler of surfaceHandlers) {
618
- const entity = frond.entities.find((e) => e.name === handler.address);
619
- const surfaceScope = scope.createScope();
620
-
621
- // Register scoped storage if output override differs from entity — under the REPOSITORY
622
- // key, which is what a Crud handler asks for, and under the port's own for a holder
623
- // that legitimately names it. Registering only the latter left a named surface with
624
- // no facade at all once the façade stopped spelling the storage.
625
- if (entity) registerSurfaceStorage(entity, handler, surfaceScope, assembly, frondLog);
560
+ /**
561
+ * One sub-scope per surface handler, holding the storage its view scopes — under the REPOSITORY
562
+ * key a Crud handler asks for. Pointing at no entity is legal here too: one rule, both surfaces.
563
+ */
564
+ function buildSurfaceFacades(building: Building, handlers: readonly HandlerEntry[]): void {
565
+ for (const handler of handlers) {
566
+ const entity = building.frond.entities.find((e) => e.name === handler.address);
567
+ const surfaceScope = building.scope.createScope();
568
+ if (entity) registerSurfaceStorage(entity, handler, surfaceScope, building.assembly, building.frondLog);
626
569
 
627
570
  const facadeKey = facadeKeyOf(handler.address, handler.surface);
628
- buildFacade(entity, handler, surfaceScope, facadeKey);
629
- frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as object).join(', ')}]`
630
- + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
571
+ buildFacadeInto(building, entity, handler, surfaceScope, facadeKey);
572
+ sayFacade(building, facadeKey, entity);
631
573
  }
574
+ }
632
575
 
633
- // A named surface is closed, so what it contains is a fact worth stating.
634
- // Saying it at boot is the difference between a rule and a rule you can
635
- // check: an entity you meant to serve and never wrote a handler for is
636
- // absent HERE, in one line, instead of being discovered missing later.
637
- const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface as string))].sort();
576
+ /** A named surface is closed, so what it serves — and what it leaves out — is said at boot, in one line. */
577
+ function sayWhatSurfacesServe(frond: FrondDescriptor, handlers: readonly HandlerEntry[], frondLog: Logger): void {
578
+ const surfaceNames = [...new Set(handlers.map((h) => h.surface as string))].sort();
638
579
  for (const surfaceName of surfaceNames) {
639
- const served = surfaceHandlers
640
- .filter((h) => h.surface === surfaceName)
641
- .map((h) => h.address)
642
- .sort();
580
+ const served = handlers.filter((h) => h.surface === surfaceName).map((h) => h.address).sort();
643
581
  const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
644
582
  frondLog.info(
645
583
  `surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
646
584
  (absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
647
585
  );
648
586
  }
587
+ }
588
+
589
+ export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
590
+ const { container, entityByName, options, log } = assembly;
591
+
592
+ if (options.remotes && frond.name in options.remotes) {
593
+ noteRemoteFrond(frond, assembly);
594
+
595
+ return;
596
+ }
597
+
598
+ // A child hangs off its parent, so what the parent registered answers here too — parents are installed first.
599
+ const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
600
+ const scope = above.createScope();
601
+ const frondLog = log.child(frond.name);
602
+ // A child of the APP logger, never of the boot's, whose `boot:` would outlive the boot.
603
+ scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
604
+
605
+ await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
606
+ const owners = ownershipOf(frond, assembly);
607
+
608
+ const declared = registerProviders(frond, scope, options.ports, assembly.boundPorts, assembly.refused, frondLog);
609
+ // An ancestor's links stand OUTSIDE this frond's own; a seam has no container key to inherit through.
610
+ const seams = inheritedSeams(frond, assembly.seamsOf, declared);
611
+ assembly.seamsOf.set(frond.name, seams);
612
+ registerStorages(frond, assembly, scope, seams, owners, frondLog);
613
+ registerFramesOf(frond, assembly, scope, frondLog);
614
+
615
+ const presenterMap = registerPresenters(frond, scope, frondLog);
616
+ const collectorTypeNames = registerCollectors(frond, scope, frondLog);
617
+ registerMiddlewares(frond, scope, assembly, frondLog);
618
+ exposePresenters(frond, presenterMap, container, scope);
619
+
620
+ const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
621
+ const defaultHandlers = frond.handlers.filter((h) => !h.surface);
622
+ const surfaceHandlers = frond.handlers.filter((h) => h.surface);
623
+ buildDefaultFacades(building, defaultHandlers);
624
+ sayUnhandledEntities(building, defaultHandlers);
625
+ buildSurfaceFacades(building, surfaceHandlers);
626
+ sayWhatSurfacesServe(frond, surfaceHandlers, frondLog);
649
627
 
650
628
  container.registerValue(`frond:${frond.name}`, scope);
651
629
  frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
@@ -6,6 +6,7 @@
6
6
  * Documented: [fronds](https://fougere.dev/docs/infra/fronds).
7
7
  */
8
8
  import type { Diagnostic } from '../diagnostic.js';
9
+ import { isFrondDescriptor } from '../descriptor/FrondDescriptor.js';
9
10
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
10
11
  import { Fronds } from '../descriptor/Fronds.js';
11
12
  import { statesModule, type FrondsStated } from '../FrondsStated.js';
@@ -102,7 +103,7 @@ export function nestingOf(
102
103
  if (frond === undefined) continue;
103
104
 
104
105
  const placed = stated[parent];
105
- const address = typeof placed === 'string' ? placed : placed?.remote;
106
+ const address = typeof placed === 'string' ? placed : isFrondDescriptor(placed) ? undefined : placed?.remote;
106
107
  if (address !== undefined || (remotes && parent in remotes)) {
107
108
  refused.push({
108
109
  severity: 'blocking',
@@ -4,7 +4,7 @@ import type { Dependent } from '../dispatch/Dependent.js';
4
4
  import type { RelationCheck } from '../dispatch/RelationCheck.js';
5
5
  import type { Hosting } from './Hosting.js';
6
6
  import type { Diagnostic } from '../diagnostic.js';
7
- import type { Releasing, Rows } from '../dispatch/Release.js';
7
+ import type { Releasing } from '../dispatch/Release.js';
8
8
  import type { EntityEntry } from '../descriptor/EntityEntry.js';
9
9
 
10
10
  /**
@@ -44,11 +44,6 @@ function referencesTo(target: string, hosting: Hosting): { entity: string; field
44
44
  return found;
45
45
  }
46
46
 
47
- /** What a target's storage has to answer for a key set to be judged in one read. */
48
- interface ByKeys {
49
- findByKeys(keys: readonly string[]): Promise<Map<string, unknown>>;
50
- }
51
-
52
47
  /**
53
48
  * The references a foreign key cannot hold, each as a read the guard makes before the write.
54
49
  *
@@ -68,7 +63,7 @@ export function heldBy(entity: SchemaView, name: string, hosting: Hosting): Rela
68
63
  field,
69
64
  target,
70
65
  missing: async (keys) => {
71
- const storage = hosting.storageOf(target) as ByKeys | undefined;
66
+ const storage = hosting.storageOf(target);
72
67
  // Nothing here holds those rows — the process that does answers the same reading,
73
68
  // so the question crosses rather than being given up on.
74
69
  if (!storage) return hosting.peerOf(target)?.missing(target, keys) ?? [];
@@ -176,7 +171,7 @@ export function unheldAmong(checks: readonly RelationCheck[], hosting: Hosting):
176
171
  export function releasing(hosting: Hosting): Releasing {
177
172
  return {
178
173
  dependentsOf: (entity) => dependentsOf(entity, hosting),
179
- rowsOf: (entity) => hosting.storageOf(entity) as Rows | undefined,
174
+ rowsOf: (entity) => hosting.storageOf(entity),
180
175
  schemaOf: (entity) => hosting.entities().get(entity),
181
176
  peers: () => hosting.peers(),
182
177
  journal: (entity) => hosting.journal(entity),
@@ -4,6 +4,7 @@
4
4
  * Documented: [the gradient](https://fougere.dev/docs/infra/gradient).
5
5
  */
6
6
  import type { FrondCall } from '../wire/FrondCall.js';
7
+ import type { StateShape } from '../wire/StateShape.js';
7
8
  import type { Transport } from '../wire/Transport.js';
8
9
  import { RPC_ENTITY } from '../wire/RpcAnswer.js';
9
10
  import { assertIdentityCard } from '../wire/card/IdentityCard.js';
@@ -103,15 +104,19 @@ export function createRemoteFacade(
103
104
  entity: string,
104
105
  router: RemoteRouter,
105
106
  middlewaresFor: (address: string) => AppMiddleware[],
107
+ shape: StateShape,
106
108
  ): Facade {
107
- const opFn = (op: string) => async (invocation: InvocationContext = Invocation.empty) => {
109
+ const opFn = (op: string) => async (received: InvocationContext = Invocation.empty) => {
108
110
  const { frond, transport, schema } = await router.route(entity);
109
111
  const call: FrondCall = { frond, entity, op };
112
+ const state = received.crossed ? { ...received.state } : shape.judge(received.state, entity, op);
113
+ const invocation = { ...received, state };
114
+ const entered = { ...state };
110
115
  const ctx: OperationContext = {
111
- entity, frond, operation: op, args: [], state: invocation.state, invocation,
116
+ entity, frond, operation: op, args: [], state, invocation,
112
117
  };
113
118
  const answer = await runMiddlewares(middlewaresFor(entity), ctx, () =>
114
- transport(call, ctx.invocation ?? invocation));
119
+ transport(call, { ...(ctx.invocation ?? invocation), state: shape.judge(ctx.state, entity, op, entered) }));
115
120
 
116
121
  // The schema the card carried, put to work: a row crosses as data and comes back through
117
122
  // the same codecs a local facade applies, so a placement does not decide what a caller
package/src/boot/seed.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { pageOf } from '../wire/Page.js';
1
2
  import { Role } from '@fougere/schema';
2
- import { facadeKeyOf } from '../wire/Facade.js';
3
3
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
4
4
  import type { SeedEntry } from '../descriptor/SeedEntry.js';
5
5
  import type { SeedFactory } from '../descriptor/SeedFactory.js';
@@ -64,14 +64,9 @@ function whatEachWaitsFor(
64
64
  }));
65
65
  }
66
66
 
67
- /** Where a seed writes, and what it may skip — resolved per entity. */
68
- interface SeedFacade {
69
- list(): Promise<unknown[]>;
70
- write(item: unknown): Promise<unknown>;
71
- }
72
-
73
67
  /**
74
- * Plant a set of seeds, in the order given.
68
+ * Plant a set of seeds, in the order given — by the storage, as a handler writes: a seed is the
69
+ * frond's own code, not a client.
75
70
  *
76
71
  * Documented: [seeds](https://fougere.dev/docs/business/seeds).
77
72
  */
@@ -81,26 +76,26 @@ export async function runSeeds(
81
76
  report: (message: string) => void = () => {},
82
77
  ): Promise<void> {
83
78
  for (const seed of seeds) {
84
- const resolve = <T>(name: string) => app.resolve<T>(name + 'Handler');
85
- const data = typeof seed.data === 'function' ? await (seed.data as SeedFactory)(resolve) : seed.data;
86
-
87
- const facade = facadeFor(app, seed.entityName);
88
- if (!facade) {
89
- report(` ${seed.entityName}: no handler façade nor storage — skipping seed`);
79
+ const storage = app.storageFor(seed.entityName);
80
+ if (!storage) {
81
+ report(` ${seed.entityName}: no storage in this process — skipping seed`);
90
82
  continue;
91
83
  }
92
84
 
93
- const existing = await facade.list();
85
+ const existing = pageOf(await storage.list()).items;
94
86
  if (existing.length > 0) {
95
87
  report(` ${seed.entityName}: skipped (${existing.length} exist)`);
96
88
  continue;
97
89
  }
98
90
 
91
+ const resolve = <T>(name: string) => app.resolve<T>(name + 'Handler');
92
+ const data = typeof seed.data === 'function' ? await (seed.data as SeedFactory)(resolve) : seed.data;
93
+
99
94
  // The driver's error alone names neither the entity nor the row — and a foreign
100
95
  // key is exactly what a seed gets wrong.
101
96
  for (const item of data) {
102
97
  try {
103
- await facade.write(item);
98
+ await storage.create(item);
104
99
  } catch (cause) {
105
100
  throw new Error(
106
101
  `Seed '${seed.entityName}' failed on ${JSON.stringify(item)}: ${(cause as Error)?.message ?? cause}`,
@@ -112,28 +107,6 @@ export async function runSeeds(
112
107
  }
113
108
  }
114
109
 
115
- /** A seed is not a client. */
116
- function facadeFor(app: App, entityName: string): SeedFacade | undefined {
117
- let handler: Record<string, Function> | undefined;
118
- try { handler = app.resolve<Record<string, Function>>(facadeKeyOf(entityName)); } catch {}
119
-
120
- const list = handler?.list;
121
- const create = handler?.create;
122
- if (typeof list === 'function' && typeof create === 'function') {
123
- return {
124
- list: () => list.call(handler) as Promise<unknown[]>,
125
- write: (item) => create.call(handler, { params: {}, query: {}, input: item, state: {} }),
126
- };
127
- }
128
-
129
- const storage = app.storageFor(entityName) as
130
- | { list: () => Promise<unknown[]>; create: (input: unknown) => Promise<unknown> }
131
- | undefined;
132
- if (!storage) return undefined;
133
-
134
- return { list: () => storage.list(), write: (item) => storage.create(item) };
135
- }
136
-
137
110
  /**
138
111
  * The framework's own ascent for rows, named — so a host can REPLACE it rather than take over the
139
112
  * whole post-boot to get its own.
@@ -1,3 +1,5 @@
1
+ import type { Field } from '@fougere/schema';
2
+
1
3
  /**
2
4
  * An extension this frond declares — what rises and falls with the process, written beside the
3
5
  * code it instruments.
@@ -10,6 +12,19 @@ export interface ExtensionEntry {
10
12
  /** Its own `name`, or the file's — the ascent REPLACES a name already declared. */
11
13
  name: string;
12
14
  /** The value handed to the ascent, the same shape `CreateAppOptions.extensions` takes. */
13
- extension: { name?: string; up?: (app: never) => void | Promise<void>; down?: (app: never) => void | Promise<void> };
15
+ extension: {
16
+ name?: string;
17
+ state?: Readonly<Record<string, Field>>;
18
+ up?: (app: never) => void | Promise<void>;
19
+ down?: (app: never) => void | Promise<void>;
20
+ };
14
21
  filePath: string;
15
22
  }
23
+
24
+ /** Recognized by its FORM: a value stating `up`, `down`, or the `state` members it declares. */
25
+ export function isExtension(value: unknown): value is ExtensionEntry['extension'] {
26
+ const shape = value as { up?: unknown; down?: unknown; state?: unknown } | null;
27
+
28
+ return typeof shape?.up === 'function' || typeof shape?.down === 'function'
29
+ || (typeof shape?.state === 'object' && shape.state !== null);
30
+ }
@@ -63,3 +63,10 @@ export interface FrondDescriptor {
63
63
  rest?: { method?: string; path?: string; status?: number };
64
64
  }>;
65
65
  }
66
+
67
+ /** Recognized by its FORM, like everything a config holds: a name and the lists every frond carries. */
68
+ export function isFrondDescriptor(value: unknown): value is FrondDescriptor {
69
+ const shape = value as Partial<Record<keyof FrondDescriptor, unknown>> | null;
70
+
71
+ return typeof shape?.name === 'string' && Array.isArray(shape.entities) && Array.isArray(shape.handlers);
72
+ }
@@ -41,6 +41,16 @@ export class Fronds extends Array<FrondDescriptor> {
41
41
  return this.flatMap((frond) => frond.entities.map((e) => e.name)).sort();
42
42
  }
43
43
 
44
+ /**
45
+ * Whether a named surface serves an entity, as its owning frond declared it — `undefined` when
46
+ * that frond declares no such surface, and a facade registered there answers as it is.
47
+ */
48
+ admits(surface: string, entity: string): boolean | undefined {
49
+ const declared = this.owner(entity)?.surfaces?.[surface];
50
+
51
+ return declared?.some((name) => name.toLowerCase() === entity.toLowerCase());
52
+ }
53
+
44
54
  /** The names a call may address, sorted — what a NOT_FOUND must print. */
45
55
  servedNames(surface?: string): string[] {
46
56
  return this.flatMap((frond) => frond.handlers
@@ -23,6 +23,7 @@ export { type TypeRef } from '../wire/TypeRef.js';
23
23
  export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
24
24
  export type { Signature } from '../wire/Signature.js';
25
25
  export { awaitKeyOf, emitKeyOf } from '../wire/Emit.js';
26
+ export { addressOf } from '../wire/Facade.js';
26
27
  export { storageKeyOf } from '../storage/Storage.js';
27
28
  export { getPresenterFields } from '../prefab/presenter.js';
28
29
  export { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
@@ -1,6 +1,7 @@
1
1
  import type { InvocationContext } from '../wire/InvocationContext.js';
2
2
  import type { BindingPlan } from '../wire/binding.js';
3
3
  import type { CollectorLookup } from './CollectorLookup.js';
4
+ import { collectedAs } from '../prefab/collector.js';
4
5
 
5
6
  /** Resolves an operation's declared binding plan against one invocation. */
6
7
  export class ArgumentResolver {
@@ -13,7 +14,7 @@ export class ArgumentResolver {
13
14
  switch (binding.source.kind) {
14
15
  case 'collector': {
15
16
  const collector = this.collectors?.(binding.source.typeName);
16
- args.push(collector ? await collector.collect(ctx) : undefined);
17
+ args.push(collector ? collectedAs(collector, await collector.collect(ctx)) : undefined);
17
18
  break;
18
19
  }
19
20
  case 'context': {
@@ -3,6 +3,7 @@ import { type AppMiddleware } from '../wire/AppMiddleware.js';
3
3
  import type { EffectiveOperationsMap } from '../EffectiveOperationsMap.js';
4
4
  import type { HandlerEntry } from '../descriptor/HandlerEntry.js';
5
5
  import type { PresenterEntry } from '../descriptor/PresenterEntry.js';
6
+ import type { StateShape } from '../wire/StateShape.js';
6
7
 
7
8
  /** What boot resolved around one handler, beyond the handler and the scope it resolves in. */
8
9
  export interface Facade {
@@ -22,4 +23,6 @@ export interface Facade {
22
23
  presenterScope: Container;
23
24
  /** The middlewares that apply to this address, read at call time and never at boot. */
24
25
  middlewares: () => AppMiddleware[];
26
+ /** What a call's `state` may hold in this process. */
27
+ state: StateShape;
25
28
  }