@fougere/core 0.8.3-alpha.0 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) 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 +144 -9
  7. package/dist/boot/Emissions.js.map +1 -1
  8. package/dist/boot/auth.d.ts +1 -1
  9. package/dist/boot/auth.d.ts.map +1 -1
  10. package/dist/boot/bootstrap.d.ts.map +1 -1
  11. package/dist/boot/bootstrap.js +401 -598
  12. package/dist/boot/bootstrap.js.map +1 -1
  13. package/dist/boot/card.js +1 -1
  14. package/dist/boot/card.js.map +1 -1
  15. package/dist/boot/hosted.d.ts +1 -1
  16. package/dist/boot/hosted.d.ts.map +1 -1
  17. package/dist/boot/hosted.js +1 -0
  18. package/dist/boot/hosted.js.map +1 -1
  19. package/dist/boot/install.d.ts +42 -0
  20. package/dist/boot/install.d.ts.map +1 -0
  21. package/dist/boot/install.js +327 -0
  22. package/dist/boot/install.js.map +1 -0
  23. package/dist/boot/ownership.d.ts +1 -1
  24. package/dist/boot/ownership.d.ts.map +1 -1
  25. package/dist/boot/ownership.js +14 -1
  26. package/dist/boot/ownership.js.map +1 -1
  27. package/dist/boot/ports.d.ts +12 -3
  28. package/dist/boot/ports.d.ts.map +1 -1
  29. package/dist/boot/ports.js +51 -21
  30. package/dist/boot/ports.js.map +1 -1
  31. package/dist/boot/remote.d.ts.map +1 -1
  32. package/dist/boot/remote.js +3 -3
  33. package/dist/boot/remote.js.map +1 -1
  34. package/dist/boot/seed.d.ts +7 -1
  35. package/dist/boot/seed.d.ts.map +1 -1
  36. package/dist/boot/seed.js +7 -2
  37. package/dist/boot/seed.js.map +1 -1
  38. package/dist/boot/statement-drift.d.ts +1 -1
  39. package/dist/boot/statement-drift.d.ts.map +1 -1
  40. package/dist/boot/together.d.ts +1 -1
  41. package/dist/boot/together.d.ts.map +1 -1
  42. package/dist/boot/together.js +1 -1
  43. package/dist/boot/together.js.map +1 -1
  44. package/dist/boot/types.d.ts +18 -3
  45. package/dist/boot/types.d.ts.map +1 -1
  46. package/dist/builtin/LogLine.d.ts +38 -0
  47. package/dist/builtin/LogLine.d.ts.map +1 -0
  48. package/dist/builtin/LogLine.js +37 -0
  49. package/dist/builtin/LogLine.js.map +1 -0
  50. package/dist/builtin/logger.d.ts +57 -0
  51. package/dist/builtin/logger.d.ts.map +1 -1
  52. package/dist/builtin/logger.js +72 -17
  53. package/dist/builtin/logger.js.map +1 -1
  54. package/dist/config-loader.d.ts +3 -2
  55. package/dist/config-loader.d.ts.map +1 -1
  56. package/dist/config-loader.js.map +1 -1
  57. package/dist/contract.d.ts +3 -7
  58. package/dist/contract.d.ts.map +1 -1
  59. package/dist/contract.js +3 -4
  60. package/dist/contract.js.map +1 -1
  61. package/dist/{scan/conventions.d.ts → conventions.d.ts} +6 -0
  62. package/dist/conventions.d.ts.map +1 -0
  63. package/dist/{scan/conventions.js → conventions.js} +3 -2
  64. package/dist/conventions.js.map +1 -0
  65. package/dist/crypto/encoding.d.ts +0 -1
  66. package/dist/crypto/encoding.d.ts.map +1 -1
  67. package/dist/crypto/encoding.js +0 -7
  68. package/dist/crypto/encoding.js.map +1 -1
  69. package/dist/declare.d.ts +11 -1
  70. package/dist/declare.d.ts.map +1 -1
  71. package/dist/declare.js +10 -1
  72. package/dist/declare.js.map +1 -1
  73. package/dist/descriptor/frond.d.ts +34 -0
  74. package/dist/descriptor/frond.d.ts.map +1 -1
  75. package/dist/descriptor/index.d.ts +15 -0
  76. package/dist/descriptor/index.d.ts.map +1 -0
  77. package/dist/descriptor/index.js +14 -0
  78. package/dist/descriptor/index.js.map +1 -0
  79. package/dist/descriptor/surface.d.ts +8 -0
  80. package/dist/descriptor/surface.d.ts.map +1 -0
  81. package/dist/descriptor/surface.js +16 -0
  82. package/dist/descriptor/surface.js.map +1 -0
  83. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  84. package/dist/dispatch/ArgumentResolver.js +6 -2
  85. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  86. package/dist/dispatch/HandlerFacade.d.ts +2 -0
  87. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  88. package/dist/dispatch/HandlerFacade.js +9 -4
  89. package/dist/dispatch/HandlerFacade.js.map +1 -1
  90. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  91. package/dist/dispatch/StorageGuard.js +9 -23
  92. package/dist/dispatch/StorageGuard.js.map +1 -1
  93. package/dist/dispatch/presenterArguments.d.ts +7 -1
  94. package/dist/dispatch/presenterArguments.d.ts.map +1 -1
  95. package/dist/dispatch/presenterArguments.js +15 -4
  96. package/dist/dispatch/presenterArguments.js.map +1 -1
  97. package/dist/dispatch/validateInput.d.ts.map +1 -1
  98. package/dist/dispatch/validateInput.js +2 -2
  99. package/dist/dispatch/validateInput.js.map +1 -1
  100. package/dist/effective-operation.d.ts +1 -1
  101. package/dist/effective-operation.d.ts.map +1 -1
  102. package/dist/effective-operation.js +13 -19
  103. package/dist/effective-operation.js.map +1 -1
  104. package/dist/frond-config.d.ts +24 -0
  105. package/dist/frond-config.d.ts.map +1 -1
  106. package/dist/frond-config.js.map +1 -1
  107. package/dist/index.d.ts +35 -40
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +17 -14
  110. package/dist/index.js.map +1 -1
  111. package/dist/node.d.ts +2 -12
  112. package/dist/node.d.ts.map +1 -1
  113. package/dist/node.js +2 -12
  114. package/dist/node.js.map +1 -1
  115. package/dist/prefab/crud.d.ts +1 -1
  116. package/dist/prefab/crud.d.ts.map +1 -1
  117. package/dist/prefab/mirror.d.ts +10 -11
  118. package/dist/prefab/mirror.d.ts.map +1 -1
  119. package/dist/prefab/mirror.js +1 -27
  120. package/dist/prefab/mirror.js.map +1 -1
  121. package/dist/prefab/repository.d.ts +1 -1
  122. package/dist/prefab/repository.d.ts.map +1 -1
  123. package/dist/{scan/result.d.ts → scan.d.ts} +2 -2
  124. package/dist/scan.d.ts.map +1 -0
  125. package/dist/scan.js +2 -0
  126. package/dist/scan.js.map +1 -0
  127. package/dist/source.d.ts +1 -1
  128. package/dist/source.d.ts.map +1 -1
  129. package/dist/storage/criterion.d.ts.map +1 -0
  130. package/dist/{criterion.js → storage/criterion.js} +2 -2
  131. package/dist/storage/criterion.js.map +1 -0
  132. package/dist/{storage.d.ts → storage/port.d.ts} +6 -3
  133. package/dist/storage/port.d.ts.map +1 -0
  134. package/dist/{storage.js → storage/port.js} +13 -6
  135. package/dist/storage/port.js.map +1 -0
  136. package/dist/{store.d.ts → storage/store.d.ts} +1 -1
  137. package/dist/storage/store.d.ts.map +1 -0
  138. package/dist/{store.js → storage/store.js} +33 -17
  139. package/dist/storage/store.js.map +1 -0
  140. package/dist/verify.js +1 -1
  141. package/dist/verify.js.map +1 -1
  142. package/dist/wire/Invocation.d.ts +1 -3
  143. package/dist/wire/Invocation.d.ts.map +1 -1
  144. package/dist/wire/Invocation.js +1 -5
  145. package/dist/wire/Invocation.js.map +1 -1
  146. package/dist/wire/binding.d.ts +9 -0
  147. package/dist/wire/binding.d.ts.map +1 -1
  148. package/dist/wire/binding.js +11 -6
  149. package/dist/wire/binding.js.map +1 -1
  150. package/dist/wire/emit.d.ts +45 -0
  151. package/dist/wire/emit.d.ts.map +1 -0
  152. package/dist/wire/emit.js +44 -0
  153. package/dist/wire/emit.js.map +1 -0
  154. package/dist/wire/errors.js +2 -2
  155. package/dist/wire/errors.js.map +1 -1
  156. package/dist/wire/loggerMiddleware.d.ts.map +1 -1
  157. package/dist/wire/loggerMiddleware.js +6 -0
  158. package/dist/wire/loggerMiddleware.js.map +1 -1
  159. package/package.json +10 -4
  160. package/src/boot/AppLifecycle.ts +8 -0
  161. package/src/boot/Emissions.ts +177 -9
  162. package/src/boot/auth.ts +1 -1
  163. package/src/boot/bootstrap.ts +456 -672
  164. package/src/boot/card.ts +1 -1
  165. package/src/boot/hosted.ts +4 -2
  166. package/src/boot/install.ts +421 -0
  167. package/src/boot/ownership.ts +17 -2
  168. package/src/boot/ports.ts +71 -27
  169. package/src/boot/remote.ts +3 -3
  170. package/src/boot/seed.ts +18 -3
  171. package/src/boot/statement-drift.ts +1 -1
  172. package/src/boot/together.ts +1 -1
  173. package/src/boot/types.ts +18 -3
  174. package/src/builtin/LogLine.ts +38 -0
  175. package/src/builtin/logger.ts +109 -17
  176. package/src/config-loader.ts +3 -2
  177. package/src/contract.ts +5 -9
  178. package/src/{scan/conventions.ts → conventions.ts} +9 -2
  179. package/src/crypto/encoding.ts +0 -7
  180. package/src/declare.ts +20 -2
  181. package/src/descriptor/frond.ts +31 -0
  182. package/src/descriptor/index.ts +15 -0
  183. package/src/descriptor/surface.ts +23 -0
  184. package/src/dispatch/ArgumentResolver.ts +6 -2
  185. package/src/dispatch/HandlerFacade.ts +11 -4
  186. package/src/dispatch/StorageGuard.ts +8 -21
  187. package/src/dispatch/presenterArguments.ts +21 -9
  188. package/src/dispatch/validateInput.ts +2 -2
  189. package/src/effective-operation.ts +16 -21
  190. package/src/frond-config.ts +20 -0
  191. package/src/index.ts +44 -57
  192. package/src/node.ts +2 -16
  193. package/src/prefab/crud.ts +1 -1
  194. package/src/prefab/mirror.ts +12 -38
  195. package/src/prefab/repository.ts +1 -1
  196. package/src/{scan/result.ts → scan.ts} +1 -1
  197. package/src/source.ts +1 -1
  198. package/src/{criterion.ts → storage/criterion.ts} +2 -2
  199. package/src/{storage.ts → storage/port.ts} +20 -5
  200. package/src/{store.ts → storage/store.ts} +35 -16
  201. package/src/verify.ts +1 -1
  202. package/src/wire/Invocation.ts +2 -7
  203. package/src/wire/binding.ts +17 -6
  204. package/src/wire/call.ts +1 -1
  205. package/src/wire/emit.ts +84 -0
  206. package/src/wire/errors.ts +2 -2
  207. package/src/wire/loggerMiddleware.ts +6 -0
  208. package/dist/boot/boot.d.ts +0 -45
  209. package/dist/boot/boot.d.ts.map +0 -1
  210. package/dist/boot/boot.js +0 -63
  211. package/dist/boot/boot.js.map +0 -1
  212. package/dist/criterion.d.ts.map +0 -1
  213. package/dist/criterion.js.map +0 -1
  214. package/dist/declarations.d.ts +0 -34
  215. package/dist/declarations.d.ts.map +0 -1
  216. package/dist/declarations.js +0 -75
  217. package/dist/declarations.js.map +0 -1
  218. package/dist/emit.d.ts +0 -13
  219. package/dist/emit.d.ts.map +0 -1
  220. package/dist/emit.js +0 -21
  221. package/dist/emit.js.map +0 -1
  222. package/dist/imports.d.ts +0 -16
  223. package/dist/imports.d.ts.map +0 -1
  224. package/dist/imports.js +0 -78
  225. package/dist/imports.js.map +0 -1
  226. package/dist/placement.d.ts +0 -25
  227. package/dist/placement.d.ts.map +0 -1
  228. package/dist/placement.js +0 -69
  229. package/dist/placement.js.map +0 -1
  230. package/dist/scan/adapters.d.ts +0 -3
  231. package/dist/scan/adapters.d.ts.map +0 -1
  232. package/dist/scan/adapters.js +0 -25
  233. package/dist/scan/adapters.js.map +0 -1
  234. package/dist/scan/bundling.d.ts +0 -3
  235. package/dist/scan/bundling.d.ts.map +0 -1
  236. package/dist/scan/bundling.js +0 -15
  237. package/dist/scan/bundling.js.map +0 -1
  238. package/dist/scan/contract.d.ts +0 -27
  239. package/dist/scan/contract.d.ts.map +0 -1
  240. package/dist/scan/contract.js +0 -46
  241. package/dist/scan/contract.js.map +0 -1
  242. package/dist/scan/conventions.d.ts.map +0 -1
  243. package/dist/scan/conventions.js.map +0 -1
  244. package/dist/scan/emit.d.ts +0 -10
  245. package/dist/scan/emit.d.ts.map +0 -1
  246. package/dist/scan/emit.js +0 -144
  247. package/dist/scan/emit.js.map +0 -1
  248. package/dist/scan/handler-parser.d.ts +0 -26
  249. package/dist/scan/handler-parser.d.ts.map +0 -1
  250. package/dist/scan/handler-parser.js +0 -607
  251. package/dist/scan/handler-parser.js.map +0 -1
  252. package/dist/scan/result.d.ts.map +0 -1
  253. package/dist/scan/result.js +0 -2
  254. package/dist/scan/result.js.map +0 -1
  255. package/dist/scan/scanner.d.ts +0 -12
  256. package/dist/scan/scanner.d.ts.map +0 -1
  257. package/dist/scan/scanner.js +0 -573
  258. package/dist/scan/scanner.js.map +0 -1
  259. package/dist/scan/statement.d.ts +0 -4
  260. package/dist/scan/statement.d.ts.map +0 -1
  261. package/dist/scan/statement.js +0 -135
  262. package/dist/scan/statement.js.map +0 -1
  263. package/dist/storage.d.ts.map +0 -1
  264. package/dist/storage.js.map +0 -1
  265. package/dist/store.d.ts.map +0 -1
  266. package/dist/store.js.map +0 -1
  267. package/src/boot/boot.ts +0 -110
  268. package/src/declarations.ts +0 -110
  269. package/src/emit.ts +0 -32
  270. package/src/imports.ts +0 -94
  271. package/src/placement.ts +0 -86
  272. package/src/scan/adapters.ts +0 -32
  273. package/src/scan/bundling.ts +0 -14
  274. package/src/scan/contract.ts +0 -80
  275. package/src/scan/emit.ts +0 -160
  276. package/src/scan/handler-parser.ts +0 -695
  277. package/src/scan/scanner.ts +0 -698
  278. package/src/scan/statement.ts +0 -159
  279. /package/dist/{criterion.d.ts → storage/criterion.d.ts} +0 -0
@@ -1,9 +1,9 @@
1
- import { applyCreate, 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 '../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
  }
@@ -160,7 +318,17 @@ export class Emissions {
160
318
  private describeRefusal(fact: string, cause: unknown): string | undefined {
161
319
  const refusals = validationErrorsOf(cause);
162
320
  if (!refusals?.length) return undefined;
163
- return `refused the shape — ${refusals.map((d) => `${d.path}: ${d.message}`).join(', ')}.`
321
+ return `refused the shape — ${refusals.map((d) => `${dotted(d.path)}: ${d.message}`).join(', ')}.`
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
+ }
package/src/boot/auth.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * the contract between core and an `@fougere/auth-*` package.
4
4
  */
5
5
  import type { SchemaView } from '@fougere/schema';
6
- import type { Storage, StorageFactory } from '../storage.js';
6
+ import type { Storage, StorageFactory } from '../storage/port.js';
7
7
 
8
8
  /** Lazy auth declaration written in fougere.config.ts. */
9
9
  export interface AuthConfig {