@fougere/core 0.8.4-alpha.0 → 0.9.0-alpha.1

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 (116) hide show
  1. package/dist/boot/AppLifecycle.d.ts +8 -0
  2. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  3. package/dist/boot/AppLifecycle.js.map +1 -1
  4. package/dist/boot/Emissions.d.ts +26 -0
  5. package/dist/boot/Emissions.d.ts.map +1 -1
  6. package/dist/boot/Emissions.js +143 -8
  7. package/dist/boot/Emissions.js.map +1 -1
  8. package/dist/boot/bootstrap.d.ts.map +1 -1
  9. package/dist/boot/bootstrap.js +83 -18
  10. package/dist/boot/bootstrap.js.map +1 -1
  11. package/dist/boot/card.d.ts.map +1 -1
  12. package/dist/boot/card.js +6 -1
  13. package/dist/boot/card.js.map +1 -1
  14. package/dist/boot/hosted.d.ts.map +1 -1
  15. package/dist/boot/hosted.js +1 -0
  16. package/dist/boot/hosted.js.map +1 -1
  17. package/dist/boot/install.d.ts +2 -0
  18. package/dist/boot/install.d.ts.map +1 -1
  19. package/dist/boot/install.js +33 -4
  20. package/dist/boot/install.js.map +1 -1
  21. package/dist/boot/ports.d.ts +12 -3
  22. package/dist/boot/ports.d.ts.map +1 -1
  23. package/dist/boot/ports.js +51 -21
  24. package/dist/boot/ports.js.map +1 -1
  25. package/dist/boot/remote.d.ts.map +1 -1
  26. package/dist/boot/remote.js +3 -3
  27. package/dist/boot/remote.js.map +1 -1
  28. package/dist/boot/types.d.ts +16 -1
  29. package/dist/boot/types.d.ts.map +1 -1
  30. package/dist/builtin/LogLine.d.ts +38 -0
  31. package/dist/builtin/LogLine.d.ts.map +1 -0
  32. package/dist/builtin/LogLine.js +37 -0
  33. package/dist/builtin/LogLine.js.map +1 -0
  34. package/dist/builtin/logger.d.ts +57 -0
  35. package/dist/builtin/logger.d.ts.map +1 -1
  36. package/dist/builtin/logger.js +72 -17
  37. package/dist/builtin/logger.js.map +1 -1
  38. package/dist/config-loader.d.ts +2 -1
  39. package/dist/config-loader.d.ts.map +1 -1
  40. package/dist/config-loader.js.map +1 -1
  41. package/dist/contract.d.ts +3 -7
  42. package/dist/contract.d.ts.map +1 -1
  43. package/dist/contract.js +3 -4
  44. package/dist/contract.js.map +1 -1
  45. package/dist/conventions.d.ts +6 -0
  46. package/dist/conventions.d.ts.map +1 -1
  47. package/dist/conventions.js +3 -2
  48. package/dist/conventions.js.map +1 -1
  49. package/dist/declare.d.ts +11 -1
  50. package/dist/declare.d.ts.map +1 -1
  51. package/dist/declare.js +9 -0
  52. package/dist/declare.js.map +1 -1
  53. package/dist/descriptor/frond.d.ts +34 -0
  54. package/dist/descriptor/frond.d.ts.map +1 -1
  55. package/dist/descriptor/index.d.ts +1 -1
  56. package/dist/descriptor/index.d.ts.map +1 -1
  57. package/dist/descriptor/index.js +1 -1
  58. package/dist/descriptor/index.js.map +1 -1
  59. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  60. package/dist/dispatch/ArgumentResolver.js +6 -2
  61. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  62. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  63. package/dist/dispatch/HandlerFacade.js +2 -2
  64. package/dist/dispatch/HandlerFacade.js.map +1 -1
  65. package/dist/effective-operation.d.ts.map +1 -1
  66. package/dist/effective-operation.js +11 -7
  67. package/dist/effective-operation.js.map +1 -1
  68. package/dist/frond-config.d.ts +24 -0
  69. package/dist/frond-config.d.ts.map +1 -1
  70. package/dist/frond-config.js.map +1 -1
  71. package/dist/index.d.ts +30 -33
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +14 -13
  74. package/dist/index.js.map +1 -1
  75. package/dist/wire/Invocation.d.ts +1 -3
  76. package/dist/wire/Invocation.d.ts.map +1 -1
  77. package/dist/wire/Invocation.js +1 -5
  78. package/dist/wire/Invocation.js.map +1 -1
  79. package/dist/wire/binding.d.ts +9 -0
  80. package/dist/wire/binding.d.ts.map +1 -1
  81. package/dist/wire/binding.js +11 -6
  82. package/dist/wire/binding.js.map +1 -1
  83. package/dist/wire/emit.d.ts +35 -3
  84. package/dist/wire/emit.d.ts.map +1 -1
  85. package/dist/wire/emit.js +26 -3
  86. package/dist/wire/emit.js.map +1 -1
  87. package/dist/wire/loggerMiddleware.d.ts.map +1 -1
  88. package/dist/wire/loggerMiddleware.js +6 -0
  89. package/dist/wire/loggerMiddleware.js.map +1 -1
  90. package/package.json +4 -4
  91. package/src/boot/AppLifecycle.ts +8 -0
  92. package/src/boot/Emissions.ts +176 -8
  93. package/src/boot/bootstrap.ts +95 -17
  94. package/src/boot/card.ts +6 -1
  95. package/src/boot/hosted.ts +3 -1
  96. package/src/boot/install.ts +35 -4
  97. package/src/boot/ports.ts +71 -27
  98. package/src/boot/remote.ts +3 -3
  99. package/src/boot/types.ts +16 -1
  100. package/src/builtin/LogLine.ts +38 -0
  101. package/src/builtin/logger.ts +109 -17
  102. package/src/config-loader.ts +2 -1
  103. package/src/contract.ts +5 -9
  104. package/src/conventions.ts +9 -2
  105. package/src/declare.ts +19 -1
  106. package/src/descriptor/frond.ts +31 -0
  107. package/src/descriptor/index.ts +1 -1
  108. package/src/dispatch/ArgumentResolver.ts +6 -2
  109. package/src/dispatch/HandlerFacade.ts +2 -2
  110. package/src/effective-operation.ts +13 -7
  111. package/src/frond-config.ts +20 -0
  112. package/src/index.ts +36 -50
  113. package/src/wire/Invocation.ts +2 -7
  114. package/src/wire/binding.ts +17 -6
  115. package/src/wire/emit.ts +58 -6
  116. package/src/wire/loggerMiddleware.ts +6 -0
@@ -17,14 +17,19 @@ export function computeBindingPlan(params, collectorTypeNames) {
17
17
  // scan spells it, and the two agree on one word only — `AuthorUser` looked up as
18
18
  // `authoruser` missed `authorUser` and fell through to branch 4, the request input.
19
19
  const typeKey = lowerFirst(typeName);
20
- // 0. Fact `Fact<X>` names itself, so nothing has to be known in advance. It comes
21
- // FIRST because branch 4 would otherwise hand it the caller's input under the name
22
- // of something that happened.
23
- const factOf = param.type.name === 'Fact' ? param.type.generics?.[0]?.name : undefined;
24
- if (factOf) {
20
+ // 0. Fact, or the same fact before it is final. Both name themselves, so nothing has
21
+ // to be known in advance, and they come FIRST because branch 4 would otherwise hand
22
+ // either the caller's input under the name of something that happened.
23
+ const announced = param.type.name === 'Fact' || param.type.name === 'Pipe'
24
+ ? param.type.generics?.[0]?.name
25
+ : undefined;
26
+ if (announced) {
25
27
  return {
26
28
  name: param.name,
27
- source: { kind: 'fact', factName: lowerFirst(factOf) },
29
+ source: {
30
+ kind: (param.type.name === 'Pipe' ? 'pipe' : 'fact'),
31
+ factName: lowerFirst(announced),
32
+ },
28
33
  optional: param.optional ?? false,
29
34
  };
30
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../src/wire/binding.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAsB7C,iDAAiD;AAEjD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AAE5D,SAAS,WAAW,CAAC,QAAgB;IACnC,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC3C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC7C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iDAAiD;AAEjD,qDAAqD;AACrD,MAAM,UAAU,kBAAkB,CAChC,MAAe,EACf,kBAA+B;IAE/B,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,8EAA8E;QAC9E,iFAAiF;QACjF,oFAAoF;QACpF,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAErC,oFAAoF;QACpF,sFAAsF;QACtF,iCAAiC;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACvF,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,EAAE,IAAI,EAAE,MAAe,EAAE,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE;gBAC/D,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,sEAAsE;QACtE,IAAI,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,EAAE,IAAI,EAAE,WAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACzD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,iDAAiD;QACjD,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;YACrC,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,EAAE,IAAI,EAAE,SAAkB,EAAE;gBACpC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,sDAAsD;QACtD,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE;oBACN,IAAI,EAAE,OAAgB;oBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC;iBAC9B;gBACD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,6BAA6B;QAC7B,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,EAAE,IAAI,EAAE,OAAgB,EAAE;YAClC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;SAClC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../src/wire/binding.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA4B7C,iDAAiD;AAEjD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AAE5D,SAAS,WAAW,CAAC,QAAgB;IACnC,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC3C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC7C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iDAAiD;AAEjD,qDAAqD;AACrD,MAAM,UAAU,kBAAkB,CAChC,MAAe,EACf,kBAA+B;IAE/B,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,8EAA8E;QAC9E,iFAAiF;QACjF,oFAAoF;QACpF,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAErC,qFAAqF;QACrF,uFAAuF;QACvF,0EAA0E;QAC1E,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM;YACxE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI;YAChC,CAAC,CAAC,SAAS,CAAC;QACd,IAAI,SAAS,EAAE,CAAC;YACd,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE;oBACN,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAoB;oBACvE,QAAQ,EAAE,UAAU,CAAC,SAAS,CAAC;iBAChC;gBACD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,sEAAsE;QACtE,IAAI,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,EAAE,IAAI,EAAE,WAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACzD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,iDAAiD;QACjD,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;YACrC,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,EAAE,IAAI,EAAE,SAAkB,EAAE;gBACpC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,sDAAsD;QACtD,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE;oBACN,IAAI,EAAE,OAAgB;oBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC;iBAC9B;gBACD,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;aAClC,CAAC;QACJ,CAAC;QAED,6BAA6B;QAC7B,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,EAAE,IAAI,EAAE,OAAgB,EAAE;YAClC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;SAClC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1,13 +1,45 @@
1
- /** What an emitter injects. */
2
- export type Emit<T> = (fact: T) => Promise<void>;
3
- /** What a subscriber accepts and what it PROMISES about itself. */
1
+ /**
2
+ * What an emitter injects. PARTIAL, because announcing is what REALIZES the fact's
3
+ * `lifecycle.create` an `at: created()` is stamped by `Emissions`, so requiring it from
4
+ * the announcer made every emitter cast its way past its own type. A missing field is
5
+ * still refused, by the judge that reads the fact at announce time.
6
+ */
7
+ export type Emit<T, A = never> = (fact: Partial<T>) => Promise<A[]>;
8
+ /**
9
+ * What a subscriber accepts — and what it PROMISES about itself.
10
+ *
11
+ * What it ANSWERS is its own to choose: `Promise<void>` says nothing, any other type is an
12
+ * opinion. It reaches an announcer that asked for one — `Emit<CanBook, Verdict>` — and
13
+ * nobody otherwise, since a plain announcement does not wait.
14
+ */
4
15
  export type Fact<T> = T;
16
+ /**
17
+ * The same fact, BEFORE it is final: an op taking one ANSWERS the value every subscriber
18
+ * then receives, so it is the declared form of what `Emissions.stamped` already does for
19
+ * `created()`.
20
+ *
21
+ * Not a second announcement and not a chain a subscriber joins: the links run once, before
22
+ * anyone is handed anything, which is what keeps a fact the same for every reader.
23
+ */
24
+ export type Pipe<T> = T;
5
25
  /** The container key of an emission — THE one place that spells the format. */
6
26
  export declare function emitKeyOf(fact: string): string;
27
+ /**
28
+ * The key of an emission that WAITS — `Emit<CanBook, Verdict>`, where the second type is
29
+ * the whole difference. A separate key because they are two functions: one hands the fact
30
+ * over and returns, the other waits for every subscriber and gives back what they said.
31
+ */
32
+ export declare function awaitKeyOf(fact: string): string;
7
33
  /** The fact behind an emission key, or `undefined` when the key is not one. */
8
34
  export declare function factOfEmitKey(key: string): string | undefined;
35
+ /** The fact behind an awaited emission's key — the dual, read the same way. */
36
+ export declare function factOfAwaitKey(key: string): string | undefined;
9
37
  /** What a set of handlers ANNOUNCES — `Emit<T>` read back out of their dependencies. */
10
38
  export declare function factsAnnouncedBy(handlers: readonly {
11
39
  deps: readonly string[];
12
40
  }[]): string[];
41
+ /** What a set of handlers announces AND WAITS FOR — the same reading, one suffix apart. */
42
+ export declare function factsAwaitedBy(handlers: readonly {
43
+ deps: readonly string[];
44
+ }[]): string[];
13
45
  //# sourceMappingURL=emit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/wire/emit.ts"],"names":[],"mappings":"AAGA,+BAA+B;AAC/B,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjD,qEAAqE;AACrE,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAExB,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAID,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI7D;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS;IAAE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,CAO3F"}
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/wire/emit.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAExB;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAExB,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAKD,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE7D;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE9D;AAQD,wFAAwF;AACxF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS;IAAE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,CAE3F;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS;IAAE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,CAEzF"}
package/dist/wire/emit.js CHANGED
@@ -4,18 +4,41 @@ import { lowerFirst } from '@fougere/schema';
4
4
  export function emitKeyOf(fact) {
5
5
  return `${lowerFirst(fact)}Emit`;
6
6
  }
7
+ /**
8
+ * The key of an emission that WAITS — `Emit<CanBook, Verdict>`, where the second type is
9
+ * the whole difference. A separate key because they are two functions: one hands the fact
10
+ * over and returns, the other waits for every subscriber and gives back what they said.
11
+ */
12
+ export function awaitKeyOf(fact) {
13
+ return `${lowerFirst(fact)}Await`;
14
+ }
7
15
  const SUFFIX = 'Emit';
16
+ const AWAIT_SUFFIX = 'Await';
8
17
  /** The fact behind an emission key, or `undefined` when the key is not one. */
9
18
  export function factOfEmitKey(key) {
10
- return key.length > SUFFIX.length && key.endsWith(SUFFIX)
11
- ? key.slice(0, -SUFFIX.length)
19
+ return behind(key, SUFFIX);
20
+ }
21
+ /** The fact behind an awaited emission's key — the dual, read the same way. */
22
+ export function factOfAwaitKey(key) {
23
+ return behind(key, AWAIT_SUFFIX);
24
+ }
25
+ function behind(key, suffix) {
26
+ return key.length > suffix.length && key.endsWith(suffix)
27
+ ? key.slice(0, -suffix.length)
12
28
  : undefined;
13
29
  }
14
30
  /** What a set of handlers ANNOUNCES — `Emit<T>` read back out of their dependencies. */
15
31
  export function factsAnnouncedBy(handlers) {
32
+ return factsIn(handlers, factOfEmitKey);
33
+ }
34
+ /** What a set of handlers announces AND WAITS FOR — the same reading, one suffix apart. */
35
+ export function factsAwaitedBy(handlers) {
36
+ return factsIn(handlers, factOfAwaitKey);
37
+ }
38
+ function factsIn(handlers, behindKey) {
16
39
  return [...new Set(handlers
17
40
  .flatMap((handler) => handler.deps)
18
- .map(factOfEmitKey)
41
+ .map(behindKey)
19
42
  .filter((fact) => fact !== undefined))];
20
43
  }
21
44
  //# sourceMappingURL=emit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"emit.js","sourceRoot":"","sources":["../../src/wire/emit.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQ7C,+EAA+E;AAC/E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;AACnC,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QACvD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QAC9B,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,QAAgD;IAC/E,OAAO,CAAC,GAAG,IAAI,GAAG,CAChB,QAAQ;aACL,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,GAAG,CAAC,aAAa,CAAC;aAClB,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CACxD,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"emit.js","sourceRoot":"","sources":["../../src/wire/emit.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA6B7C,+EAA+E;AAC/E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;AACpC,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,CAAC;AACtB,MAAM,YAAY,GAAG,OAAO,CAAC;AAE7B,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,MAAM,CAAC,GAAW,EAAE,MAAc;IACzC,OAAO,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QACvD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QAC9B,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,QAAgD;IAC/E,OAAO,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AAC1C,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,cAAc,CAAC,QAAgD;IAC7E,OAAO,OAAO,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,OAAO,CACd,QAAgD,EAChD,SAA8C;IAE9C,OAAO,CAAC,GAAG,IAAI,GAAG,CAChB,QAAQ;aACL,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,GAAG,CAAC,SAAS,CAAC;aACd,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CACxD,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"loggerMiddleware.d.ts","sourceRoot":"","sources":["../../src/wire/loggerMiddleware.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAe9D"}
1
+ {"version":3,"file":"loggerMiddleware.d.ts","sourceRoot":"","sources":["../../src/wire/loggerMiddleware.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAoB9D"}
@@ -1,6 +1,12 @@
1
+ import { CARRIES_LINE } from '../builtin/LogLine.js';
1
2
  /** Create a logger middleware that logs operation entry, exit, and errors. */
2
3
  export function loggerMiddleware(logger) {
3
4
  return async (ctx, next) => {
5
+ // An op that CARRIES a line writes none: it is dispatched, so logging it announces a
6
+ // line inside the announcement of one — `Emission cycle: logLine → logLine`, which is
7
+ // the emission catching what `onLog`'s try/catch used to swallow.
8
+ if (CARRIES_LINE.has(ctx.entity))
9
+ return next();
4
10
  const start = performance.now();
5
11
  logger.info(`${ctx.entity}.${ctx.operation}`);
6
12
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"loggerMiddleware.js","sourceRoot":"","sources":["../../src/wire/loggerMiddleware.ts"],"names":[],"mappings":"AAMA,8EAA8E;AAC9E,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAClD,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,KAAK,EAAE,KAAK,CAAC,CAAC;YACxD,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAClD,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9D,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"loggerMiddleware.js","sourceRoot":"","sources":["../../src/wire/loggerMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,8EAA8E;AAC9E,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,qFAAqF;QACrF,sFAAsF;QACtF,kEAAkE;QAClE,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QAEhD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAClD,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,KAAK,EAAE,KAAK,CAAC,CAAC;YACxD,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAClD,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9D,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/core",
3
- "version": "0.8.4-alpha.0",
3
+ "version": "0.9.0-alpha.1",
4
4
  "description": "The core — scan, call contract, façades, remote stand-ins.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -61,12 +61,12 @@
61
61
  "dependencies": {
62
62
  "@typescript/typescript6": "^6.0.2",
63
63
  "dequal": "^2.0.3",
64
- "@fougere/container": "0.8.4-alpha.0",
65
- "@fougere/schema": "0.8.4-alpha.0"
64
+ "@fougere/container": "0.9.0-alpha.1",
65
+ "@fougere/schema": "0.9.0-alpha.1"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^22.0.0",
69
- "@fougere/compiler": "0.8.4-alpha.0"
69
+ "@fougere/compiler": "0.9.0-alpha.1"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public"
@@ -1,8 +1,16 @@
1
1
  import type { App } from './types.js';
2
+ import type { FrondDescriptor } from '../descriptor/frond.js';
2
3
 
3
4
  /** One process-level extension and its reversible lifecycle. */
4
5
  export interface Extension {
5
6
  name: string;
7
+ /**
8
+ * Fronds it brings — read BEFORE the ascent, since a frond is installed while the app is
9
+ * still being made and `up` receives one that already exists. This is how an optional
10
+ * package accepts a fact: `calls` and `observability` both want every `LogLine`, and a
11
+ * signature is the only way to ask for one.
12
+ */
13
+ fronds?: readonly FrondDescriptor[];
6
14
  up?(app: App): void | Promise<void>;
7
15
  down?(app: App): void | Promise<void>;
8
16
  }
@@ -1,9 +1,9 @@
1
- import { applyCreate, dotted, type SchemaView } from '@fougere/schema';
1
+ import { applyCreate, dotted, lowerFirst, type SchemaView } from '@fougere/schema';
2
2
  import type { Container } from '@fougere/container';
3
3
  import { validationErrorsOf } from '../wire/errors.js';
4
- import { emitKeyOf, factsAnnouncedBy } from '../wire/emit.js';
4
+ import { awaitKeyOf, emitKeyOf, factsAnnouncedBy, factsAwaitedBy } from '../wire/emit.js';
5
5
  import { ambient } from '#ambient';
6
- import { EMPTY_INVOCATION } from '../wire/Invocation.js';
6
+ import { Invocation } from '../wire/Invocation.js';
7
7
  import type { Logger } from '../builtin/logger.js';
8
8
  import type { Fronds } from '../descriptor/Fronds.js';
9
9
  import type { OperationsMap } from '../wire/operation.js';
@@ -22,6 +22,12 @@ export class Emissions {
22
22
  /** Who listens to what. Filled as each door's contracts are resolved. */
23
23
  private readonly subscribers = new Map<string, Listener[]>();
24
24
 
25
+ /** Who FINISHES a fact, in the order they run — see `orderPipes`. */
26
+ private readonly pipes = new Map<string, Listener[]>();
27
+
28
+ /** The order its OWNER declared, by fact — `pipes:` in `frond.config.ts`. */
29
+ private readonly ordered: Map<string, string[]>;
30
+
25
31
  /**
26
32
  * What is announced here, read from the DEPS and not from the subscribers: a handler declaring
27
33
  * `Emit<PostPublished>` must resolve it whether or not anybody listens, and announcing to nobody
@@ -29,6 +35,9 @@ export class Emissions {
29
35
  */
30
36
  private readonly announced: Set<string>;
31
37
 
38
+ /** What is announced AND waited for — `Emit<T, A>`, where the second type says so. */
39
+ private readonly awaited: Set<string>;
40
+
32
41
  constructor(
33
42
  fronds: Fronds,
34
43
  private readonly shapes: Map<string, SchemaView>,
@@ -37,6 +46,23 @@ export class Emissions {
37
46
  private readonly carry?: Carrier,
38
47
  ) {
39
48
  this.announced = new Set(fronds.flatMap((frond) => factsAnnouncedBy(frond.handlers)));
49
+ this.awaited = new Set(fronds.flatMap((frond) => factsAwaitedBy(frond.handlers)));
50
+ // Read from the frond that OWNS the fact: ordering is a decision about the fact, and
51
+ // a decision has one owner. A frond ordering a neighbour's fact is refused below.
52
+ this.ordered = new Map(fronds.flatMap((frond) => {
53
+ const owned = new Set(frond.entities.map((entity) => entity.name));
54
+
55
+ return Object.entries(frond.pipes ?? {}).map(([fact, order]) => {
56
+ if (!owned.has(lowerFirst(fact))) {
57
+ throw new Error(
58
+ `Frond '${frond.name}' orders the links of '${fact}', which it does not own.\n`
59
+ + ' The order belongs to the frond that declares the entity — state `pipes:` there.',
60
+ );
61
+ }
62
+
63
+ return [lowerFirst(fact), order] as const;
64
+ });
65
+ }));
40
66
  }
41
67
 
42
68
  /**
@@ -46,6 +72,10 @@ export class Emissions {
46
72
  note(contracts: OperationsMap, door: string): void {
47
73
  for (const [op, contract] of contracts) {
48
74
  for (const bound of contract.binding ?? []) {
75
+ if (bound.source.kind === 'pipe') {
76
+ this.claimPipe(bound.source.factName, { door, op });
77
+ continue;
78
+ }
49
79
  if (bound.source.kind !== 'fact') continue;
50
80
  const listeners = this.subscribers.get(bound.source.factName) ?? [];
51
81
  listeners.push({ door, op });
@@ -54,6 +84,51 @@ export class Emissions {
54
84
  }
55
85
  }
56
86
 
87
+ /** One more op that finishes this fact. What ORDER they run in is settled at `register`. */
88
+ private claimPipe(fact: string, taking: Listener): void {
89
+ const held = this.pipes.get(fact) ?? [];
90
+ if (held.some((one) => `${one.door}.${one.op}` === `${taking.door}.${taking.op}`)) return;
91
+ held.push(taking);
92
+ this.pipes.set(fact, held);
93
+ }
94
+
95
+ /**
96
+ * Put the links in the order their fact's owner declared, and refuse what it did not.
97
+ *
98
+ * Two links with no order refuse: nothing would say which finished the fact, and scan
99
+ * order is not an answer — the same reason two implementations of a port refuse. One
100
+ * link needs no declaration, because there is nothing to order.
101
+ */
102
+ private orderPipes(): void {
103
+ for (const [fact, links] of this.pipes) {
104
+ const declared = this.ordered.get(fact);
105
+ const named = (one: Listener) => `${one.door}.${one.op}`;
106
+
107
+ if (!declared) {
108
+ if (links.length < 2) continue;
109
+ throw new Error(
110
+ `${links.length} ops finish the fact '${fact}': ${links.map(named).join(', ')}.\n`
111
+ + ' They run one after another and nothing says in which order. State it on the '
112
+ + `frond that owns '${fact}':\n`
113
+ + ` export default { pipes: { ${fact}: ['FirstHandler', 'SecondHandler'] } };\n`
114
+ + ' Or make all but one accept `Fact<…>`, which changes nothing and reads it.',
115
+ );
116
+ }
117
+
118
+ const at = (one: Listener) => declared.findIndex((name) => doorOf(name) === one.door);
119
+ const unlisted = links.filter((one) => at(one) < 0);
120
+ if (unlisted.length > 0) {
121
+ throw new Error(
122
+ `${unlisted.map(named).join(', ')} finish${unlisted.length > 1 ? '' : 'es'} the fact `
123
+ + `'${fact}', and '${fact}' orders ${declared.join(', ')} — so where it runs is not said.\n`
124
+ + ' Add it to `pipes:`, or make it accept `Fact<…>`.',
125
+ );
126
+ }
127
+
128
+ this.pipes.set(fact, [...links].sort((one, other) => at(one) - at(other)));
129
+ }
130
+ }
131
+
57
132
  /** The shape a fact is validated by, when the fact is a declared entity. */
58
133
  shapeOf(fact: string): SchemaView | undefined {
59
134
  return this.shapes.get(fact);
@@ -64,18 +139,39 @@ export class Emissions {
64
139
  return [...this.subscribers.keys()];
65
140
  }
66
141
 
142
+ /** The doors that accept one fact — the addresses a middleware must leave alone. */
143
+ doorsFor(fact: string): string[] {
144
+ return (this.subscribers.get(fact) ?? []).map(({ door }) => door);
145
+ }
146
+
67
147
  /** Register one emission value per fact — announced here, or merely listened to. */
68
148
  register(): void {
149
+ this.orderPipes();
69
150
  for (const fact of new Set([...this.announced, ...this.subscribers.keys()])) {
70
151
  this.container.registerValue(emitKeyOf(fact), (raw: unknown) => this.announce(fact, raw));
71
152
  }
153
+ for (const fact of this.awaited) {
154
+ // Waiting means knowing WHO, and a carrier publishes to whoever subscribed elsewhere
155
+ // without bringing anything back — so the answers would hold this process's
156
+ // subscribers only, and say nothing about it. Refused here rather than half-answered
157
+ // at the first call: the second type is in a signature, so a boot can read it.
158
+ if (this.carry) {
159
+ throw new Error(
160
+ `'${fact}' is announced with an answer type, and this app has a carrier (\`onEmit\`).\n`
161
+ + ' Waiting means knowing who answers; a carrier reaches whoever subscribed '
162
+ + 'elsewhere and brings nothing back.\n'
163
+ + ` Name the subscribers in \`remotes:\`, or drop the second type of \`Emit<${fact}, …>\`.`,
164
+ );
165
+ }
166
+ this.container.registerValue(awaitKeyOf(fact), (raw: unknown) => this.announce(fact, raw, true));
167
+ }
72
168
  if (ambient.degraded && this.subscribers.size > 0) {
73
169
  this.log.warn('no async context on this runtime — an emission ring is not detected');
74
170
  }
75
171
  }
76
172
 
77
173
  /** Announcing. */
78
- private async announce(fact: string, raw: unknown): Promise<void> {
174
+ private async announce(fact: string, raw: unknown, waiting = false): Promise<unknown[]> {
79
175
  /**
80
176
  * A fact announced inside a frame that then rolls back is a lie, and nothing can take it back:
81
177
  * announcing is DISPATCH — every subscriber has been handed the fact and the carrier has
@@ -83,15 +179,45 @@ export class Emissions {
83
179
  */
84
180
  await ambient.beforeAnnounce(fact);
85
181
 
86
- const payload = this.stamped(fact, raw);
182
+ const payload = await this.finished(fact, this.stamped(fact, raw));
87
183
 
88
184
  /** Whoever is not in this process — and it is the ONLY way to reach them. */
89
185
  const delivery = this.carry?.(fact, payload);
90
186
  if (delivery) void Promise.resolve(delivery).catch((cause) => this.log.error(`${fact} — carrier refused it`, cause));
91
187
 
92
- for (const { door, op, done } of this.handToListeners(fact, payload)) {
93
- void done.catch((cause) => this.log.error(`${fact} → ${door}.${op}`, this.describeRefusal(fact, cause) ?? cause));
188
+ const handed = this.handToListeners(fact, payload);
189
+ if (!waiting) {
190
+ for (const { door, op, done } of handed) {
191
+ void done.catch((cause) => this.log.error(`${fact} → ${door}.${op}`, this.describeRefusal(fact, cause) ?? cause));
192
+ }
193
+
194
+ return [];
195
+ }
196
+
197
+ const settled = await Promise.allSettled(handed.map((one) => one.done));
198
+
199
+ // A subscriber that did not answer REFUSES the announcement, it does not shrink it: an
200
+ // announcer handed the survivors cannot tell three answers from two, and its own law
201
+ // then reads silence as consent — the room one would have refused gets booked.
202
+ const missing = settled.flatMap((result, at) =>
203
+ (result.status === 'rejected' ? [{ ...handed[at]!, reason: result.reason as unknown }] : []));
204
+ if (missing.length > 0) {
205
+ for (const { door, op, reason } of missing) {
206
+ this.log.error(`${fact} → ${door}.${op}`, this.describeRefusal(fact, reason) ?? reason);
207
+ }
208
+ throw new AggregateError(
209
+ missing.map((one) => one.reason),
210
+ `${fact} — ${missing.length} of ${handed.length} subscriber(s) did not answer`
211
+ + ` (${missing.map((one) => `${one.door}.${one.op}`).join(', ')}).`
212
+ + ' An announcement with an answer type waits for everyone: a partial answer would'
213
+ + ' look like a complete one.',
214
+ );
94
215
  }
216
+
217
+ // What answers NOTHING contributes nothing — a subscriber is free to have no opinion,
218
+ // and `Promise<void>` is how it says so.
219
+ return settled.flatMap((result) =>
220
+ (result.status === 'fulfilled' && result.value != null ? [result.value] : []));
95
221
  }
96
222
 
97
223
  /** Receiving. */
@@ -114,6 +240,38 @@ export class Emissions {
114
240
  }
115
241
  }
116
242
 
243
+ /**
244
+ * What the declared link answers, or the value as it stands when nothing declared one.
245
+ *
246
+ * Before anyone is handed anything, and once: every subscriber reads the same fact, which
247
+ * is the whole reason a fact can be said to be what happened. A link that refuses stops
248
+ * the announcement — it was finishing the fact, and half a fact is not one.
249
+ */
250
+ private async finished(fact: string, payload: unknown): Promise<unknown> {
251
+ let carried = payload;
252
+ for (const link of this.pipes.get(fact) ?? []) {
253
+ const facade = this.container.resolve<Record<string, Function>>(link.door);
254
+ const answered = await facade[link.op]({ ...Invocation.empty, input: carried });
255
+
256
+ // A link that answers nothing SUPPRESSES the fact — every subscriber was then handed
257
+ // `null` and crashed reading it, one message each. It is refused rather than named,
258
+ // because a fact is what HAPPENED: the announcer already said so and cannot be told
259
+ // otherwise, `Emit` returning void. Filtering belongs to whoever announces, or to
260
+ // each reader; it is not a link's to decide for everyone.
261
+ if (answered === null || answered === undefined) {
262
+ throw new Error(
263
+ `${link.door}.${link.op} finishes the fact '${fact}' and answered nothing.\n`
264
+ + ' A link says what the fact IS, it does not take it back — the announcer has '
265
+ + 'already said it happened, and nothing can tell it otherwise.\n'
266
+ + ' Return the fact, amended or as it stands.',
267
+ );
268
+ }
269
+ carried = answered;
270
+ }
271
+
272
+ return carried;
273
+ }
274
+
117
275
  /**
118
276
  * The announcement realizes the fact's own `lifecycle.create` — a `created()` stamped, an id
119
277
  * generated, a default applied.
@@ -151,7 +309,7 @@ export class Emissions {
151
309
  } catch (cause) {
152
310
  throw new Error(`${fact} → ${door} could not be reached`, { cause });
153
311
  }
154
- return facade[op]({ ...EMPTY_INVOCATION, input: payload });
312
+ return facade[op]({ ...Invocation.empty, input: payload });
155
313
  }),
156
314
  }));
157
315
  }
@@ -164,3 +322,13 @@ export class Emissions {
164
322
  + ` If '${fact}' gained a field, this copy is older than the sender's: re-run \`fougere sync\`.`;
165
323
  }
166
324
  }
325
+
326
+ /**
327
+ * The container key a declared CLASS NAME answers under — `RedactHandler` → `redactHandler`,
328
+ * the same key `facadeKeyOf` builds from an address.
329
+ */
330
+ function doorOf(declared: string): string {
331
+ const key = lowerFirst(declared);
332
+
333
+ return key.endsWith('Handler') ? key : `${key}Handler`;
334
+ }