@fougere/core 0.12.2-alpha.0 → 0.12.4-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 (126) 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 +5 -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.map +1 -1
  10. package/dist/FrondDeclaration.js +1 -5
  11. package/dist/FrondDeclaration.js.map +1 -1
  12. package/dist/Source.js +1 -1
  13. package/dist/Source.js.map +1 -1
  14. package/dist/SourceView.d.ts +0 -2
  15. package/dist/SourceView.d.ts.map +1 -1
  16. package/dist/boot/App.d.ts +2 -4
  17. package/dist/boot/App.d.ts.map +1 -1
  18. package/dist/boot/AppLifecycle.d.ts +5 -0
  19. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  20. package/dist/boot/AppLifecycle.js +19 -10
  21. package/dist/boot/AppLifecycle.js.map +1 -1
  22. package/dist/boot/AuthRuntime.d.ts +5 -12
  23. package/dist/boot/AuthRuntime.d.ts.map +1 -1
  24. package/dist/boot/AuthRuntime.js +2 -1
  25. package/dist/boot/AuthRuntime.js.map +1 -1
  26. package/dist/boot/CreateAppOptions.d.ts +0 -11
  27. package/dist/boot/CreateAppOptions.d.ts.map +1 -1
  28. package/dist/boot/Emissions.d.ts.map +1 -1
  29. package/dist/boot/Emissions.js +3 -6
  30. package/dist/boot/Emissions.js.map +1 -1
  31. package/dist/boot/Hosting.d.ts +2 -1
  32. package/dist/boot/Hosting.d.ts.map +1 -1
  33. package/dist/boot/bootstrap.d.ts.map +1 -1
  34. package/dist/boot/bootstrap.js +140 -263
  35. package/dist/boot/bootstrap.js.map +1 -1
  36. package/dist/boot/install.d.ts.map +1 -1
  37. package/dist/boot/install.js +102 -114
  38. package/dist/boot/install.js.map +1 -1
  39. package/dist/boot/relations.d.ts.map +1 -1
  40. package/dist/boot/relations.js.map +1 -1
  41. package/dist/boot/seed.d.ts.map +1 -1
  42. package/dist/boot/seed.js.map +1 -1
  43. package/dist/descriptor/Fronds.d.ts +5 -0
  44. package/dist/descriptor/Fronds.d.ts.map +1 -1
  45. package/dist/descriptor/Fronds.js +8 -0
  46. package/dist/descriptor/Fronds.js.map +1 -1
  47. package/dist/descriptor/index.d.ts +1 -0
  48. package/dist/descriptor/index.d.ts.map +1 -1
  49. package/dist/descriptor/index.js +1 -0
  50. package/dist/descriptor/index.js.map +1 -1
  51. package/dist/dispatch/InFlight.d.ts +3 -2
  52. package/dist/dispatch/InFlight.d.ts.map +1 -1
  53. package/dist/dispatch/InFlight.js +11 -1
  54. package/dist/dispatch/InFlight.js.map +1 -1
  55. package/dist/index.d.ts +2 -4
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +2 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/node.d.ts +1 -1
  60. package/dist/node.d.ts.map +1 -1
  61. package/dist/node.js +1 -1
  62. package/dist/node.js.map +1 -1
  63. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  64. package/dist/prefab/CrudConstructor.js.map +1 -1
  65. package/dist/storage/Comparison.d.ts +2 -2
  66. package/dist/storage/Comparison.d.ts.map +1 -1
  67. package/dist/storage/Comparison.js +3 -1
  68. package/dist/storage/Comparison.js.map +1 -1
  69. package/dist/wire/Emit.d.ts +0 -2
  70. package/dist/wire/Emit.d.ts.map +1 -1
  71. package/dist/wire/Emit.js +1 -1
  72. package/dist/wire/Emit.js.map +1 -1
  73. package/dist/wire/Facade.d.ts +7 -0
  74. package/dist/wire/Facade.d.ts.map +1 -1
  75. package/dist/wire/Facade.js +13 -0
  76. package/dist/wire/Facade.js.map +1 -1
  77. package/dist/wire/OperationContract.d.ts +0 -4
  78. package/dist/wire/OperationContract.d.ts.map +1 -1
  79. package/dist/wire/OperationContract.js +1 -1
  80. package/dist/wire/OperationContract.js.map +1 -1
  81. package/dist/wire/OperationsMap.d.ts +0 -3
  82. package/dist/wire/OperationsMap.d.ts.map +1 -1
  83. package/dist/wire/OperationsMap.js +0 -5
  84. package/dist/wire/OperationsMap.js.map +1 -1
  85. package/package.json +4 -4
  86. package/src/EffectiveOperationModel.ts +221 -201
  87. package/src/FougereConfig.ts +14 -16
  88. package/src/FrondDeclaration.ts +1 -7
  89. package/src/Source.ts +1 -1
  90. package/src/SourceView.ts +0 -2
  91. package/src/boot/App.ts +2 -4
  92. package/src/boot/AppLifecycle.ts +21 -10
  93. package/src/boot/AuthRuntime.ts +6 -13
  94. package/src/boot/CreateAppOptions.ts +0 -11
  95. package/src/boot/Emissions.ts +3 -7
  96. package/src/boot/Hosting.ts +2 -1
  97. package/src/boot/bootstrap.ts +168 -300
  98. package/src/boot/install.ts +120 -146
  99. package/src/boot/relations.ts +3 -8
  100. package/src/boot/seed.ts +2 -4
  101. package/src/descriptor/Fronds.ts +10 -0
  102. package/src/descriptor/index.ts +1 -0
  103. package/src/dispatch/InFlight.ts +16 -2
  104. package/src/index.ts +2 -4
  105. package/src/node.ts +1 -1
  106. package/src/prefab/CrudConstructor.ts +0 -11
  107. package/src/storage/Comparison.ts +7 -4
  108. package/src/wire/Emit.ts +1 -1
  109. package/src/wire/Facade.ts +17 -10
  110. package/src/wire/OperationContract.ts +1 -1
  111. package/src/wire/OperationsMap.ts +0 -10
  112. package/dist/boot/AuthConfig.d.ts +0 -14
  113. package/dist/boot/AuthConfig.d.ts.map +0 -1
  114. package/dist/boot/AuthConfig.js +0 -2
  115. package/dist/boot/AuthConfig.js.map +0 -1
  116. package/dist/boot/AuthContext.d.ts +0 -12
  117. package/dist/boot/AuthContext.d.ts.map +0 -1
  118. package/dist/boot/AuthContext.js +0 -2
  119. package/dist/boot/AuthContext.js.map +0 -1
  120. package/dist/storage/ComparisonName.d.ts +0 -3
  121. package/dist/storage/ComparisonName.d.ts.map +0 -1
  122. package/dist/storage/ComparisonName.js +0 -2
  123. package/dist/storage/ComparisonName.js.map +0 -1
  124. package/src/boot/AuthConfig.ts +0 -14
  125. package/src/boot/AuthContext.ts +0 -12
  126. package/src/storage/ComparisonName.ts +0 -3
@@ -457,90 +457,51 @@ function buildFacadeInto(
457
457
  ));
458
458
  }
459
459
 
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;
460
+ /** Its facades answer elsewhere, but what they LISTEN to is read here, and the bridges route with it. */
461
+ function noteRemoteFrond(frond: FrondDescriptor, { operationModel, effectiveByKey, emissions, contractsOf, log }: Assembly): void {
462
+ log.child(frond.name).info('declared remote — not hosted locally');
463
+ for (const handler of frond.handlers) {
464
+ const key = facadeKeyOf(handler.address, handler.surface);
465
+ const operations = operationModel.forHandler(handler);
466
+ effectiveByKey.set(key, operations);
467
+ emissions.note(contractsOf(operations), key);
481
468
  }
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);
469
+ }
500
470
 
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.
471
+ /**
472
+ * Who owns what, and the rule that makes owning mean something — refused before anything is
473
+ * registered, so a bad line is named here rather than by the container.
474
+ */
475
+ function ownershipOf(frond: FrondDescriptor, { entityByName, refused }: Assembly): Map<string, string> {
503
476
  sharedNames(frond, refused);
504
477
  refuseArityDrift(frond, refused);
505
478
  const owners = ownersOf(frond.providers, frond.name, refused);
506
479
  storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
507
480
  crudOnOwned(frond, owners, refused);
508
481
 
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);
482
+ return owners;
483
+ }
515
484
 
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);
485
+ /** A frame is read from the same `deps` every port is: asking for one IS declaring it. */
486
+ function registerFramesOf(frond: FrondDescriptor, assembly: Assembly, scope: Container, frondLog: Logger): void {
487
+ const { entityByName, frondOf, options, hosting, refused } = assembly;
488
+ const wanted = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
489
+ .flatMap((declared) => declared.deps.map((key) => ({ key, filePath: declared.filePath })));
490
+
491
+ registerFrames(scope, wanted, frond.providers, {
492
+ entityByName,
493
+ frondOf,
494
+ hostedHere: (name) => !(options.remotes && name in options.remotes),
495
+ storageFactory: options.storageFactory,
496
+ sourceOf: options.sourceOf,
497
+ transacts: options.transacts,
498
+ transacted: options.transacted,
499
+ hosting,
500
+ log: frondLog,
501
+ }, refused);
502
+ }
519
503
 
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]));
504
+ function registerPresenters(frond: FrondDescriptor, scope: Container, frondLog: Logger): Map<string, PresenterEntry> {
544
505
  for (const presenter of frond.presenters) {
545
506
  scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
546
507
  }
@@ -548,104 +509,117 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
548
509
  frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
549
510
  }
550
511
 
551
- // Register collectors in scope — PascalCase type name (e.g. 'UserCollector')
552
- const collectorTypeNames = new Set(frond.collectors.map((c) => c.typeName));
512
+ return new Map(frond.presenters.map((presenter) => [presenter.entityName, presenter]));
513
+ }
514
+
515
+ function registerCollectors(frond: FrondDescriptor, scope: Container, frondLog: Logger): Set<string> {
553
516
  for (const collector of frond.collectors) {
554
- const key = collectorKeyOf(collector.typeName);
555
- scope.register(key, collector.ctor, { deps: collector.deps });
517
+ scope.register(collectorKeyOf(collector.typeName), collector.ctor, { deps: collector.deps });
556
518
  }
557
519
  if (frond.collectors.length > 0) {
558
520
  frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
559
521
  }
560
522
 
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]));
523
+ return new Set(frond.collectors.map((collector) => collector.typeName));
524
+ }
570
525
 
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);
526
+ function sayFacade(building: Building, facadeKey: string, entity: EntityEntry | undefined): void {
527
+ const operations = Object.keys(building.assembly.container.resolve(facadeKey) as object).join(', ');
528
+ building.frondLog.debug(`${facadeKey} [${operations}]`
529
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
530
+ }
582
531
 
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.
532
+ /**
533
+ * A facade is about a handler, so this walks HANDLERS — pointing at no entity is legal. The
534
+ * subject is `Crud(Item)`'s when it names one, the handler's own name otherwise, compared by
535
+ * NAME: the scanner and the runtime load one class as two objects.
536
+ */
537
+ function buildDefaultFacades(building: Building, handlers: readonly HandlerEntry[]): void {
538
+ for (const handler of handlers) {
594
539
  const crudTarget = targetOf(handler.ctor);
595
540
  const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
596
- const entity = frond.entities.find((e) => e.name === subject);
541
+ const entity = building.frond.entities.find((e) => e.name === subject);
597
542
  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'));
543
+ buildFacadeInto(building, entity, handler, building.scope, facadeKey);
544
+ sayFacade(building, facadeKey, entity);
601
545
  }
546
+ }
602
547
 
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
- }
548
+ /** A shape that declares no operation answers nothing — said once per entity rather than deduced from a silence. */
549
+ function sayUnhandledEntities(building: Building, handlers: readonly HandlerEntry[]): void {
550
+ const handled = new Set(handlers.map((handler) => handler.address));
551
+ for (const entity of building.frond.entities) {
552
+ if (!handled.has(entity.name)) building.frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
609
553
  }
554
+ }
610
555
 
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);
556
+ /**
557
+ * One sub-scope per surface handler, holding the storage its view scopes — under the REPOSITORY
558
+ * key a Crud handler asks for. Pointing at no entity is legal here too: one rule, both surfaces.
559
+ */
560
+ function buildSurfaceFacades(building: Building, handlers: readonly HandlerEntry[]): void {
561
+ for (const handler of handlers) {
562
+ const entity = building.frond.entities.find((e) => e.name === handler.address);
563
+ const surfaceScope = building.scope.createScope();
564
+ if (entity) registerSurfaceStorage(entity, handler, surfaceScope, building.assembly, building.frondLog);
626
565
 
627
566
  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'));
567
+ buildFacadeInto(building, entity, handler, surfaceScope, facadeKey);
568
+ sayFacade(building, facadeKey, entity);
631
569
  }
570
+ }
632
571
 
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();
572
+ /** A named surface is closed, so what it serves — and what it leaves out — is said at boot, in one line. */
573
+ function sayWhatSurfacesServe(frond: FrondDescriptor, handlers: readonly HandlerEntry[], frondLog: Logger): void {
574
+ const surfaceNames = [...new Set(handlers.map((h) => h.surface as string))].sort();
638
575
  for (const surfaceName of surfaceNames) {
639
- const served = surfaceHandlers
640
- .filter((h) => h.surface === surfaceName)
641
- .map((h) => h.address)
642
- .sort();
576
+ const served = handlers.filter((h) => h.surface === surfaceName).map((h) => h.address).sort();
643
577
  const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
644
578
  frondLog.info(
645
579
  `surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
646
580
  (absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
647
581
  );
648
582
  }
583
+ }
584
+
585
+ export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
586
+ const { container, entityByName, options, log } = assembly;
587
+
588
+ if (options.remotes && frond.name in options.remotes) {
589
+ noteRemoteFrond(frond, assembly);
590
+
591
+ return;
592
+ }
593
+
594
+ // A child hangs off its parent, so what the parent registered answers here too — parents are installed first.
595
+ const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
596
+ const scope = above.createScope();
597
+ const frondLog = log.child(frond.name);
598
+ // A child of the APP logger, never of the boot's, whose `boot:` would outlive the boot.
599
+ scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
600
+
601
+ await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
602
+ const owners = ownershipOf(frond, assembly);
603
+
604
+ const declared = registerProviders(frond, scope, options.ports, assembly.boundPorts, assembly.refused, frondLog);
605
+ // An ancestor's links stand OUTSIDE this frond's own; a seam has no container key to inherit through.
606
+ const seams = inheritedSeams(frond, assembly.seamsOf, declared);
607
+ assembly.seamsOf.set(frond.name, seams);
608
+ registerStorages(frond, assembly, scope, seams, owners, frondLog);
609
+ registerFramesOf(frond, assembly, scope, frondLog);
610
+
611
+ const presenterMap = registerPresenters(frond, scope, frondLog);
612
+ const collectorTypeNames = registerCollectors(frond, scope, frondLog);
613
+ registerMiddlewares(frond, scope, assembly, frondLog);
614
+ exposePresenters(frond, presenterMap, container, scope);
615
+
616
+ const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
617
+ const defaultHandlers = frond.handlers.filter((h) => !h.surface);
618
+ const surfaceHandlers = frond.handlers.filter((h) => h.surface);
619
+ buildDefaultFacades(building, defaultHandlers);
620
+ sayUnhandledEntities(building, defaultHandlers);
621
+ buildSurfaceFacades(building, surfaceHandlers);
622
+ sayWhatSurfacesServe(frond, surfaceHandlers, frondLog);
649
623
 
650
624
  container.registerValue(`frond:${frond.name}`, scope);
651
625
  frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
@@ -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),
package/src/boot/seed.ts CHANGED
@@ -67,7 +67,7 @@ function whatEachWaitsFor(
67
67
  /** Where a seed writes, and what it may skip — resolved per entity. */
68
68
  interface SeedFacade {
69
69
  list(): Promise<unknown[]>;
70
- write(item: unknown): Promise<unknown>;
70
+ write(item: Record<string, unknown>): Promise<unknown>;
71
71
  }
72
72
 
73
73
  /**
@@ -126,9 +126,7 @@ function facadeFor(app: App, entityName: string): SeedFacade | undefined {
126
126
  };
127
127
  }
128
128
 
129
- const storage = app.storageFor(entityName) as
130
- | { list: () => Promise<unknown[]>; create: (input: unknown) => Promise<unknown> }
131
- | undefined;
129
+ const storage = app.storageFor(entityName);
132
130
  if (!storage) return undefined;
133
131
 
134
132
  return { list: () => storage.list(), write: (item) => storage.create(item) };
@@ -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';
@@ -36,11 +36,25 @@ export class InFlight {
36
36
  };
37
37
  }
38
38
 
39
- close(): void {
39
+ /** Close the door, and answer once the calls already running are done — or refuse at the deadline, naming what is left. */
40
+ async drain(timeoutMs?: number): Promise<void> {
40
41
  this.accepting = false;
42
+ if (timeoutMs === undefined) return this.whenIdle();
43
+
44
+ let timer: ReturnType<typeof setTimeout>;
45
+
46
+ await Promise.race([
47
+ this.whenIdle().then(() => clearTimeout(timer)),
48
+ new Promise<never>((_, reject) => {
49
+ timer = setTimeout(
50
+ () => reject(new Error(`[drain] ${this.running} call(s) still running after ${timeoutMs}ms`)),
51
+ timeoutMs,
52
+ );
53
+ }),
54
+ ]);
41
55
  }
42
56
 
43
- whenIdle(): Promise<void> {
57
+ private whenIdle(): Promise<void> {
44
58
  if (this.running === 0) return Promise.resolve();
45
59
 
46
60
  return new Promise((resolve) => this.idle.push(resolve));
package/src/index.ts CHANGED
@@ -91,7 +91,7 @@ export type { FougereOperations } from './wire/FougereOperations.js';
91
91
  export type { HandlerOf } from './wire/HandlerOf.js';
92
92
  export type { Refused } from './wire/Refused.js';
93
93
  export type { Rows } from './wire/Rows.js';
94
- export type { Facade } from './wire/Facade.js';
94
+ export { addressOf, type Facade } from './wire/Facade.js';
95
95
  export type {
96
96
  FrondCall,
97
97
  Transport,
@@ -150,9 +150,7 @@ export type { PresenterEntry } from './descriptor/PresenterEntry.js';
150
150
  export type { PresenterFieldMeta } from './descriptor/PresenterFieldMeta.js';
151
151
  export type { ProviderEntry } from './descriptor/ProviderEntry.js';
152
152
  export type { SeedEntry } from './descriptor/SeedEntry.js';
153
- export type { AuthConfig } from './boot/AuthConfig.js';
154
- export type { AuthContext } from './boot/AuthContext.js';
155
- export type { AuthRuntime } from './boot/AuthRuntime.js';
153
+ export { AUTH, type AuthRuntime } from './boot/AuthRuntime.js';
156
154
  export { Fronds } from './descriptor/Fronds.js';
157
155
  export { verify, type Misplaced } from './verify.js';
158
156
  // Same question as verify(), answered from the source text instead of the model.
package/src/node.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** What Fougere does with a filesystem, minus the scan — that is `@fougere/compiler`. */
2
2
  export { setModuleLoader, getModuleLoader } from './loader.js';
3
- export { loadConfig, loadCascadedConfig, remotesOf } from './FougereConfig.js';
3
+ export { configFileIn, loadConfig, loadCascadedConfig, remotesOf } from './FougereConfig.js';
4
4
  export { statedModules } from './StatedModules.js';
5
5
  export { defineFrond, loadFrondConfig } from './FrondConfig.js';
6
6
 
@@ -1,10 +1,8 @@
1
1
  import { lowerFirst, type EntityConstructor, type SchemaView } from '@fougere/schema';
2
2
  import type { ListOptions } from '../storage/ListOptions.js';
3
- import type { ListResult } from '../storage/ListResult.js';
4
3
  import type { Storage } from '../storage/Storage.js';
5
4
  import type { OperationContract } from '../wire/OperationContract.js';
6
5
  import { targetOf } from './prefab.js';
7
- import type { CrudOpName } from './CrudOpName.js';
8
6
  import type { CrudViews } from './CrudViews.js';
9
7
  import { pageOf, type Page } from '../wire/Page.js';
10
8
  import type { CrudOps } from './CrudOps.js';
@@ -88,15 +86,6 @@ function asCrudConstructor<T, V>(impl: object): CrudConstructor<T, V> {
88
86
  return impl as CrudConstructor<T, V>;
89
87
  }
90
88
 
91
- /** The view an op emits, fabricated. */
92
- type OutOf<V, K extends CrudOpName, T> =
93
- // Bracketed on purpose: a naked `V extends …` DISTRIBUTES, and the no-view default
94
- // is the empty map, whose `keyof` is `never` — distribution would then collapse
95
- // every op's output to `never` instead of falling through to the entity.
96
- [V] extends [EntityConstructor] ? InstanceType<V & EntityConstructor>
97
- : K extends keyof V ? (V[K] extends EntityConstructor ? InstanceType<V[K]> : T)
98
- : T;
99
-
100
89
  /** The prefab handler class — its ops, plus the statics the bootstrap and adapters read. */
101
90
  export interface CrudConstructor<T, V = {}> {
102
91
  // `Storage<T>`, not the bare `Storage`: a handler that injects a second storage has to
@@ -1,5 +1,4 @@
1
1
  import { type Field, Shapes } from '@fougere/schema';
2
- import type { ComparisonName } from './ComparisonName.js';
3
2
 
4
3
  /**
5
4
  * The comparisons a criterion may name.
@@ -21,7 +20,11 @@ export interface Comparison {
21
20
  isNull?: boolean;
22
21
  }
23
22
 
24
- export const COMPARISONS = ['gte', 'lte', 'gt', 'lt', 'ne', 'between', 'contains', 'notIn', 'isNull'] as const;
23
+ export type ComparisonName = keyof Comparison;
24
+
25
+ export const COMPARISONS = Object.keys({
26
+ gte: true, lte: true, gt: true, lt: true, ne: true, between: true, contains: true, notIn: true, isNull: true,
27
+ } satisfies Record<ComparisonName, true>);
25
28
 
26
29
  /**
27
30
  * Is this criterion a comparison, or a value that happens to be an object?
@@ -42,11 +45,11 @@ export function comparisonOf(field: Field | undefined, asked: unknown): Comparis
42
45
  /** The comparisons a criterion names, in the order the caller wrote them. */
43
46
  export function comparisonsIn(comparison: Comparison): [ComparisonName, unknown][] {
44
47
  return Object.entries(comparison).filter(
45
- (entry): entry is [ComparisonName, unknown] => (COMPARISONS as readonly string[]).includes(entry[0]),
48
+ (entry): entry is [ComparisonName, unknown] => COMPARISONS.includes(entry[0]),
46
49
  );
47
50
  }
48
51
 
49
52
  /** What a comparison names that this vocabulary does not — a typo, said as one. */
50
53
  export function unknownIn(comparison: Comparison): string[] {
51
- return Object.keys(comparison).filter((name) => !(COMPARISONS as readonly string[]).includes(name));
54
+ return Object.keys(comparison).filter((name) => !COMPARISONS.includes(name));
52
55
  }
package/src/wire/Emit.ts CHANGED
@@ -35,7 +35,7 @@ export function factOfEmitKey(key: string): string | undefined {
35
35
  }
36
36
 
37
37
  /** The fact behind an awaited emission's key — the dual, read the same way. */
38
- export function factOfAwaitKey(key: string): string | undefined {
38
+ function factOfAwaitKey(key: string): string | undefined {
39
39
  return behind(key, AWAIT_SUFFIX);
40
40
  }
41
41
 
@@ -1,14 +1,6 @@
1
- import type { InvocationContext } from './InvocationContext.js';
2
- import type { FougereOperations } from './FougereOperations.js';
3
-
4
- type Served = keyof FougereOperations & string;
1
+ import { lowerFirst } from '@fougere/schema';
5
2
 
6
- /**
7
- * Both halves below take the union through a type PARAMETER, which is what makes the
8
- * conditional distribute: `Served` is an alias, and an alias is matched whole — `'a.x' | 'b.y'`
9
- * does not extend `` `${string}.${string}` `` as one thing, so the naive form answers `never`.
10
- */
11
- type AddressIn<Key> = Key extends `${infer Address}.${string}` ? Address : never;
3
+ import type { InvocationContext } from './InvocationContext.js';
12
4
 
13
5
  /**
14
6
  * The facade built in front of a handler — the framework's second port, after `Storage`.
@@ -34,6 +26,21 @@ export function facadeKeyOf(entityName: string, surface?: string): string {
34
26
  return surface ? `${surface}:${entityName}Handler` : `${entityName}Handler`;
35
27
  }
36
28
 
29
+ /**
30
+ * The address a handler answers at, read off its class name or its facade key — the dual of
31
+ * `facadeKeyOf`. `PostHandler` → `post`, `postHandler` → `post`.
32
+ */
33
+ export function addressOf(name: string): string {
34
+ return lowerFirst(name.endsWith(HANDLER) ? name.slice(0, -HANDLER.length) : name);
35
+ }
36
+
37
+ /** A default facade's key — `postHandler`, never `admin:postHandler` nor `PostStorage`. */
38
+ export function isFacadeKey(key: string): boolean {
39
+ return key.endsWith(HANDLER) && !key.includes(':');
40
+ }
41
+
42
+ const HANDLER = 'Handler';
43
+
37
44
  /** Where the contracts behind a façade live — the dual of `facadeKeyOf`. */
38
45
  export function contractsKeyOf(entityName: string, surface?: string): string {
39
46
  return `${facadeKeyOf(entityName, surface)}:contracts`;
@@ -117,7 +117,7 @@ export function inferOperationKind(name: string): OperationKindInference {
117
117
  }
118
118
 
119
119
  /** Explicit config is authoritative; otherwise the finite convention may decline. */
120
- export function resolveOperationKind(
120
+ function resolveOperationKind(
121
121
  name: string,
122
122
  overrides?: Record<string, { kind?: OperationKind }>,
123
123
  ): OperationKind | undefined {
@@ -1,14 +1,4 @@
1
- import type { Param } from './Param.js';
2
- import type { TypeRef } from './TypeRef.js';
3
1
  import type { OperationContract } from './OperationContract.js';
4
2
 
5
3
  /** Map of operation name → its contract. */
6
4
  export type OperationsMap = Map<string, OperationContract>;
7
-
8
- export type { TypeRef, Param };
9
-
10
- // ─── Operation intent (read vs write) ──────────
11
- // Naming convention for inferred handlers — scheduled to die with the
12
- // handler-kind plan (docs/notes/handler-kind.md). Lives here, NOT in
13
- // @fougere/schema: it is a runtime convention about operations, not a
14
- // schema concept.
@@ -1,14 +0,0 @@
1
- import type { SchemaView } from '@fougere/schema';
2
- import type { AuthContext } from './AuthContext.js';
3
- import type { AuthRuntime } from './AuthRuntime.js';
4
- /** Lazy auth declaration written in fougere.config.ts. */
5
- export interface AuthConfig {
6
- /** Build the runtime — invoked by createApp at boot with the resolved storage handles. */
7
- create(ctx: AuthContext): AuthRuntime | Promise<AuthRuntime>;
8
- /**
9
- * Entities the provider will use. Optional metadata — useful for the core's
10
- * future migration registry to know which tables auth needs.
11
- */
12
- entities?: Record<string, SchemaView>;
13
- }
14
- //# sourceMappingURL=AuthConfig.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AuthConfig.d.ts","sourceRoot":"","sources":["../../src/boot/AuthConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,0FAA0F;IAC1F,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7D;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACvC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=AuthConfig.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../src/boot/AuthConfig.ts"],"names":[],"mappings":""}