@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
package/src/boot/card.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** What an app says about ITSELF — the `rpc.discover` answer, built from the app. */
2
2
  import { Card } from '@fougere/schema';
3
3
  import type { App } from './types.js';
4
- import { factsAnnouncedBy } from '../emit.js';
4
+ import { factsAnnouncedBy } from '../wire/emit.js';
5
5
  import type { InvocationContext } from '../wire/Invocation.js';
6
6
  import { facadeKeyOf, contractsKeyOf, type CardOp, type IdentityCard } from '../wire/call.js';
7
7
  import type { OperationContract } from '../wire/operation.js';
@@ -1,6 +1,6 @@
1
1
  /** What this app hosts — what it STATED, or what a scan FOUND. */
2
2
  import type { FrondDescriptor } from '../descriptor/frond.js';
3
- import type { ScanResult } from '../scan/result.js';
3
+ import type { ScanResult } from '../scan.js';
4
4
  import { Fronds } from '../descriptor/Fronds.js';
5
5
 
6
6
  /** Where an app's fronds come from — either, or both. */
@@ -12,7 +12,8 @@ export interface HostedSources {
12
12
  }
13
13
 
14
14
  /** The members a frond takes from a scan when it named none of its own. */
15
- type Fillable = 'providers' | 'entities' | 'handlers' | 'presenters' | 'collectors' | 'seeds';
15
+ type Fillable = 'providers' | 'entities' | 'handlers' | 'presenters' | 'collectors' | 'seeds'
16
+ | 'middlewares';
16
17
 
17
18
  /** A frond the host passed, completed by what a scan found under the same name. */
18
19
  function completed(given: FrondDescriptor, found: FrondDescriptor | undefined): FrondDescriptor {
@@ -33,6 +34,7 @@ function completed(given: FrondDescriptor, found: FrondDescriptor | undefined):
33
34
  presenters: fill('presenters'),
34
35
  collectors: fill('collectors'),
35
36
  seeds: fill('seeds'),
37
+ middlewares: fill('middlewares'),
36
38
  };
37
39
  }
38
40
 
@@ -0,0 +1,421 @@
1
+ /** Putting one frond into the app being built: its scope, what it serves, what it takes. */
2
+ import type { Container } from '@fougere/container';
3
+ import { lowerFirst, type Fields, type SchemaView } from '@fougere/schema';
4
+ import type { Logger } from '../builtin/logger.js';
5
+ import type { Dispatcher } from '../dispatch/Dispatcher.js';
6
+ import type { RouteRegistry } from '../dispatch/RouteRegistry.js';
7
+ import type { Emissions } from './Emissions.js';
8
+ import type { EffectiveOperationsMap, EffectiveOperationModel } from '../effective-operation.js';
9
+ import { nameOf } from '../descriptor/frond.js';
10
+ import type { EntityEntry, FrondDescriptor, HandlerEntry } from '../descriptor/frond.js';
11
+ import type { OperationsMap } from '../wire/operation.js';
12
+ import type { AppMiddleware } from '../wire/middleware.js';
13
+ import type { CreateAppOptions } from './types.js';
14
+ import { registerFrames } from './together.js';
15
+ import { HandlerFacade } from '../dispatch/HandlerFacade.js';
16
+ import { targetOf } from '../prefab/prefab.js';
17
+ import { ownersOf, refuseStorageInUserCode, refuseCrudOnOwned } from './ownership.js';
18
+ import { StorageGuard } from '../dispatch/StorageGuard.js';
19
+ import { portBindings } from './ports.js';
20
+ import { facadeKeyOf, contractsKeyOf } from '../wire/call.js';
21
+ import { inheritsCrud, subjectOf } from '../prefab/crud.js';
22
+ import { repositoryKeyOf } from '../prefab/repository.js';
23
+ import { storageKeyOf } from '../storage/port.js';
24
+ import { declares } from '../source.js';
25
+ import { presenterKeyOf } from '../prefab/presenter.js';
26
+ import { collectorKeyOf } from '../prefab/collector.js';
27
+ import { RouteAddress } from '../wire/RouteAddress.js';
28
+ import { servedSurfaces } from '../descriptor/surface.js';
29
+ import { OperationRoute } from '../dispatch/OperationRoute.js';
30
+ import { facadeOperations } from '../entry/facade.js';
31
+
32
+ /**
33
+ * What the app is made of while it is still being made. `createApp` builds these, hands
34
+ * them to every frond in turn, and the App it returns is assembled FROM them — so a frond
35
+ * adds to the same container, the same routes and the same tables the next one will read.
36
+ */
37
+ export interface Assembly {
38
+ container: Container;
39
+ routeRegistry: RouteRegistry;
40
+ emissions: Emissions;
41
+ dispatcher: Dispatcher;
42
+ /** A door under a named surface answers only what that surface serves. */
43
+ localDispatcher: Dispatcher;
44
+ /** Filled per façade key, read back by `operationsFor` and the identity card. */
45
+ effectiveByKey: Map<string, EffectiveOperationsMap>;
46
+ /** Every `ports:` key some frond actually settled — what is left is a typo. */
47
+ boundPorts: Set<string>;
48
+ /** What the model resolved before the boot performed any side effect. */
49
+ operationModel: EffectiveOperationModel;
50
+ entityByName: Map<string, SchemaView>;
51
+ frondOf: Map<string, string>;
52
+ contractsOf: (operations: EffectiveOperationsMap) => OperationsMap;
53
+ /** Read at call time and never at boot, so a late registration still applies. */
54
+ getMiddlewares: (entity: string) => AppMiddleware[];
55
+ /** Take a middleware on — every entity when no entity is named. */
56
+ use: (middleware: AppMiddleware, entity?: string) => void;
57
+ log: Logger;
58
+ options: CreateAppOptions;
59
+ }
60
+
61
+ export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
62
+ const {
63
+ container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
64
+ boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
65
+ log, options,
66
+ } = assembly;
67
+
68
+ // Declared remote: keep the scanned metadata (bridges route with it),
69
+ // register nothing locally — resolve() falls through to the remote façade.
70
+ if (options.remotes && frond.name in options.remotes) {
71
+ log.child(frond.name).info('declared remote — not hosted locally');
72
+ // Its doors answer elsewhere, but what they LISTEN to was read here.
73
+ for (const handler of frond.handlers) {
74
+ const key = facadeKeyOf(handler.address, handler.surface);
75
+ const operations = operationModel.forHandler(handler);
76
+ effectiveByKey.set(key, operations);
77
+ emissions.note(contractsOf(operations), key);
78
+
79
+ }
80
+ return;
81
+ }
82
+ const scope = container.createScope();
83
+ const frondLog = log.child(frond.name);
84
+
85
+ // `reads:` is what makes a cross-source reader exist here, and the list IS its
86
+ // environment — a source holding none of these is never opened. Registered under
87
+ // the type's own name, which is the key `depKeyOf` already derives for a plain
88
+ // parameter: `constructor(private reads: Reads)` and nothing else to say.
89
+ // Declaring `reads:` with nothing to build the reader is a boot that ignores a
90
+ // clause: the handler asking for `Reads` then dies at its first call, on a
91
+ // container message that names neither the clause nor what is missing.
92
+ if (frond.reads?.length && !options.sourcesFactory) {
93
+ frondLog.warn(
94
+ `[reads] ${frond.reads.join(', ')} — declared in frond.config.ts, but this boot passes no `
95
+ + '`sourcesFactory`, so no reader is registered and a handler asking for `Reads` will fail '
96
+ + 'at its first call. Pass one (`@fougere/adapter-duckdb`), or drop the clause.',
97
+ );
98
+ }
99
+ if (frond.reads?.length && options.sourcesFactory) {
100
+ // Resolved across the WHOLE app, not this frond's own entities: a cross-source
101
+ // query joins entities from different fronds by definition — `Progress` here,
102
+ // `Book` next door — so restricting the list to its own would make it useless.
103
+ // Naming one IS the authorization; that is what the declaration is for.
104
+ const named = frond.reads
105
+ .map((name) => entityByName.get(lowerFirst(name)))
106
+ .filter((entity): entity is NonNullable<typeof entity> => entity !== undefined);
107
+ if (named.length !== frond.reads.length) {
108
+ const missing = frond.reads.filter((name) => !entityByName.has(lowerFirst(name)));
109
+ frondLog.warn(
110
+ `[reads] ${missing.join(', ')} — named in frond.config.ts but scanned nowhere in this app, `
111
+ + 'so a query naming one would find no table. Check the spelling, or the entity file.',
112
+ );
113
+ }
114
+ scope.registerValue('Reads', await options.sourcesFactory(named, frond.name));
115
+ frondLog.debug(`cross-source reader over ${named.length} entit(ies)`);
116
+ }
117
+
118
+ // Who owns what, and the rule that makes owning mean something. Before anything is
119
+ // registered, so a bad line is named by this refusal rather than by the container's.
120
+ const owners = ownersOf(frond.providers);
121
+ refuseStorageInUserCode(frond, owners, (entity) => entityByName.has(entity));
122
+ refuseCrudOnOwned(frond, owners);
123
+
124
+ for (const provider of frond.providers) {
125
+ scope.register(nameOf(provider), provider.ctor, { deps: provider.deps });
126
+ }
127
+ // …and again under the port each one extends, so `private payment: Payment`
128
+ // reaches the realization instead of the base class it is declared against.
129
+ // Registered AFTER the loop above so a port key always wins over the base's
130
+ // own registration — same precedence as a declared repository over its default.
131
+ for (const [port, chain] of portBindings(frond.providers, (n) => scope.has(n), options.ports)) {
132
+ // Registered from the INSIDE OUT, each wrapper asking for the one it stands in front
133
+ // of: the container resolves a dep by NAME, so wrapping is a substituted key and
134
+ // needs nothing of the container itself. The outermost answers under the port.
135
+ let inner = nameOf(chain.at(-1)!);
136
+ for (const wrapper of [...chain.slice(0, -1)].reverse()) {
137
+ const deps = wrapper.deps.map((dep) => (dep === port ? inner : dep));
138
+ inner = nameOf(wrapper);
139
+ scope.register(inner, wrapper.ctor, { deps });
140
+ }
141
+ const outermost = chain[0]!;
142
+ scope.register(port, outermost.ctor, {
143
+ deps: outermost.deps.map((dep) => (dep === port ? nameOf(chain[1]!) : dep)),
144
+ });
145
+ boundPorts.add(port);
146
+ frondLog.debug(`port ${port} → ${chain.map((one) => one.ctor.name).join(' → ')}`);
147
+ }
148
+ if (frond.providers.length > 0) {
149
+ frondLog.debug(`${frond.providers.length} provider(s): ${frond.providers.map(nameOf).join(', ')}`);
150
+ }
151
+
152
+ // Register Storage for each entity — PascalCase type name (e.g. 'PostStorage')
153
+ // When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
154
+ if (options.storageFactory) {
155
+ const unenforced: string[] = [];
156
+ for (const entity of frond.entities) {
157
+ const key = storageKeyOf(entity.name);
158
+ const source = options.sourceOf?.(entity.name) ?? 'db';
159
+ if (declares(entity.entityClass, 'unique') && options.enforces?.(source, 'unique') === false) {
160
+ unenforced.push(`${entity.name} in '${source}'`);
161
+ }
162
+ const baseStorage = options.storageFactory(entity.entityClass, entity.name);
163
+
164
+ // Check if the default handler (no surface) declares an output override
165
+ const defaultHandler = frond.handlers.find((h) => h.address === entity.name && !h.surface);
166
+ const outputSchema = defaultHandler?.outputOverride ?? (defaultHandler?.ctor as any)?.__output;
167
+ const scoped = outputSchema && outputSchema !== entity.entityClass
168
+ ? baseStorage.output(outputSchema)
169
+ : baseStorage;
170
+
171
+ // Storage is a way out like the client surface — see `StorageGuard`.
172
+ const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name).guard(scoped);
173
+ scope.registerValue(key, guarded);
174
+
175
+ // The default repository IS the guarded port — it already answers every gesture a
176
+ // declared one forwards, so the two forms have the same shape and a handler reads
177
+ // `repo.list()` either way. The wrapper that used to sit here (`{ storage: guarded }`)
178
+ // existed to make `repo.storage` true in both, back when `.storage` was the way in.
179
+ //
180
+ // Not registered for an OWNED entity: an aggregate's members are reached through it
181
+ // and nowhere else, and the default would be a second door under a name a handler
182
+ // can spell. Every member is skipped, not just the one the key is named after —
183
+ // that asymmetry was the whole hole.
184
+ const repoKey = repositoryKeyOf(entity.name);
185
+ const owner = owners.get(entity.name);
186
+ if (owner) {
187
+ frondLog.debug(`${entity.name} — owned by ${owner}, no default repository`);
188
+ } else if (!scope.has(repoKey)) {
189
+ scope.registerValue(repoKey, guarded);
190
+ }
191
+ }
192
+ if (frond.entities.length > 0) {
193
+ frondLog.debug(`${frond.entities.length} entity storage(s): ${frond.entities.map((e) => e.name).join(', ')}`);
194
+ }
195
+ // The judge refuses a duplicate it can SEE — the row already stored. Two writes arriving
196
+ // together see the same absence, and only the place they land can refuse the second.
197
+ if (unenforced.length > 0) {
198
+ frondLog.warn(
199
+ `unique declared, and the source does not enforce it: ${unenforced.join(', ')} — `
200
+ + 'two concurrent writes can both pass',
201
+ );
202
+ }
203
+ }
204
+
205
+ // Frames, after the ORMs and before anything that may ask for one. A frame is read
206
+ // from the same `deps` every other port is read from — asking for it IS declaring it,
207
+ // so nothing is registered for a frame nobody wants.
208
+ registerFrames(
209
+ scope,
210
+ [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors].flatMap((d) => d.deps),
211
+ frond.providers,
212
+ {
213
+ entityByName,
214
+ frondOf,
215
+ hostedHere: (name) => !(options.remotes && name in options.remotes),
216
+ storageFactory: options.storageFactory,
217
+ sourceOf: options.sourceOf,
218
+ transacts: options.transacts,
219
+ transacted: options.transacted,
220
+ log: frondLog,
221
+ },
222
+ );
223
+
224
+ // Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
225
+ const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
226
+ for (const presenter of frond.presenters) {
227
+ scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
228
+ }
229
+ if (frond.presenters.length > 0) {
230
+ frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
231
+ }
232
+
233
+ // Register collectors in scope — PascalCase type name (e.g. 'UserCollector')
234
+ const collectorTypeNames = new Set(frond.collectors.map((c) => c.typeName));
235
+ for (const collector of frond.collectors) {
236
+ const key = collectorKeyOf(collector.typeName);
237
+ scope.register(key, collector.ctor, { deps: collector.deps });
238
+ }
239
+ if (frond.collectors.length > 0) {
240
+ frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
241
+ }
242
+
243
+ // Register middlewares in scope, then take them on. Resolved per call and never here:
244
+ // a middleware asking for something request-scoped would otherwise be handed the one
245
+ // instance the boot built — the same reason `getMiddlewares` is read at call time.
246
+ for (const middleware of frond.middlewares) {
247
+ scope.register(middleware.name, middleware.ctor, { deps: middleware.deps });
248
+ const around: AppMiddleware = (context, next) =>
249
+ scope.resolve<{ around: AppMiddleware }>(middleware.name).around(context, next);
250
+
251
+ if (middleware.scope === 'app') use(around);
252
+ // Its own frond means every address its handlers answer to — wider than its entities,
253
+ // since a handler without one runs behind it too.
254
+ else for (const address of new Set(frond.handlers.map((h) => h.address))) use(around, address);
255
+ }
256
+ if (frond.middlewares.length > 0) {
257
+ frondLog.debug(`${frond.middlewares.length} middleware(s): ${frond.middlewares.map((m) => `${m.name} (${m.scope})`).join(', ')}`);
258
+ }
259
+
260
+ // Build handler facades → registered in ROOT container (public contract)
261
+ const defaultHandlers = frond.handlers.filter((h) => !h.surface);
262
+ const surfaceHandlers = frond.handlers.filter((h) => h.surface);
263
+ const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
264
+
265
+ /** Build the door of a handler and register it under the audience it serves. */
266
+ const buildFacade = (
267
+ entity: EntityEntry | undefined,
268
+ handler: HandlerEntry,
269
+ targetScope: Container,
270
+ facadeKey: string,
271
+ ) => {
272
+ if (inheritsCrud(handler.ctor) && !entity) {
273
+ // An installed Crud subject may be absent from the local scan.
274
+ frondLog.debug(`${handler.ctor.name} extends Crud() and no scanned entity is named `
275
+ + `'${subjectOf(handler.ctor, handler.address)}' — installed entity, or a missing `
276
+ + `one: no storage will be injected`);
277
+ }
278
+
279
+ const facade = new HandlerFacade(handler, targetScope, {
280
+ key: facadeKey,
281
+ frond: frond.name,
282
+ handlers: frond.handlers,
283
+ operations: operationModel.forHandler(handler),
284
+ collectors: collectorTypeNames,
285
+ presenter: presenterMap.get(handler.address),
286
+ presenterScope: scope,
287
+ middlewares: () => getMiddlewares(handler.address),
288
+ });
289
+ // Emissions use the same contracts and execution path as direct calls.
290
+ emissions.note(facade.contracts, facadeKey);
291
+ // The terms alongside the door, under the same audience — a surface that serves
292
+ // fewer ops describes fewer ops.
293
+ container.registerValue(contractsKeyOf(handler.address, handler.surface), facade.contracts);
294
+ effectiveByKey.set(facadeKey, facade.effectiveOperations);
295
+
296
+ const surfaces = servedSurfaces(frond, handler);
297
+
298
+ for (const operation of facade.contracts.keys()) {
299
+ for (const surface of surfaces) {
300
+ const address = new RouteAddress({
301
+ entity: handler.address,
302
+ operation,
303
+ ...(surface !== undefined ? { surface } : {}),
304
+ });
305
+ routeRegistry.register(new OperationRoute(
306
+ 'local',
307
+ address,
308
+ (call) => facade.execute(operation, call.invocation),
309
+ ));
310
+ }
311
+ }
312
+
313
+ const operations = facadeOperations(
314
+ handler.surface ? localDispatcher : dispatcher,
315
+ handler.address,
316
+ routeRegistry.operationNames(handler.address, handler.surface),
317
+ handler.surface,
318
+ );
319
+ container.registerValue(facadeKey, operations);
320
+ };
321
+
322
+ // A presenter is about an entity — computed fields sit on a shape — so this walks
323
+ // entities. Exposing the instance lazily; the bridge resolves it on first access.
324
+ for (const entity of frond.entities) {
325
+ if (!presenterMap.has(entity.name)) continue;
326
+ const presenterKey = presenterKeyOf(entity.name);
327
+ let presenterInstance: any;
328
+ container.registerValue(presenterKey, new Proxy({} as any, {
329
+ get(_target, prop) {
330
+ if (!presenterInstance) presenterInstance = scope.resolve(presenterKey);
331
+ return presenterInstance[prop];
332
+ },
333
+ }));
334
+ }
335
+
336
+ // A facade is about a handler, so this walks HANDLERS. It walked entities before,
337
+ // which made an entity a precondition for being callable at all: a handler naming
338
+ // none was scanned, then never built, and nothing said so.
339
+ for (const handler of defaultHandlers) {
340
+ // Two ways to know the subject, and the explicit one wins: `Crud(Item)` names the
341
+ // entity it was built on, whatever the handler is called. Otherwise the handler's
342
+ // own name is the only thing pointing at one — and pointing at nothing is legal.
343
+ //
344
+ // By NAME, not by identity: the scanner loads an entity through its own loader and
345
+ // the handler imports it through the runtime's, so the same class arrives as two
346
+ // objects. `===` compares module instances, which is not the question being asked.
347
+ const crudTarget = targetOf(handler.ctor);
348
+ const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
349
+ const entity = frond.entities.find((e) => e.name === subject);
350
+ const facadeKey = facadeKeyOf(handler.address);
351
+ buildFacade(entity, handler, scope, facadeKey);
352
+ frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
353
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
354
+ }
355
+
356
+ // The dual, and it stays: a shape that declares no operation answers nothing. Said
357
+ // once per entity rather than deduced from a silence.
358
+ for (const entity of frond.entities) {
359
+ if (!defaultHandlerMap.has(entity.name)) {
360
+ frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
361
+ }
362
+ }
363
+
364
+ // Surface handlers — create sub-scope per surface handler with scoped storage
365
+ //
366
+ // Pointing at nothing is legal HERE TOO. This loop used to `continue` when no entity
367
+ // carried the handler's name, so `handlers/public/SearchHandler.ts` with no `Search`
368
+ // entity got no door at all and no line saying why — while the very same handler at
369
+ // the default surface is built and logged. One rule, both surfaces.
370
+ for (const handler of surfaceHandlers) {
371
+ const entity = frond.entities.find((e) => e.name === handler.address);
372
+ const surfaceScope = scope.createScope();
373
+
374
+ // Register scoped storage if output override differs from entity — under the REPOSITORY
375
+ // key, which is what a Crud handler asks for, and under the port's own for a holder
376
+ // that legitimately names it. Registering only the latter left a named surface with
377
+ // no door at all once the façade stopped spelling the storage.
378
+ if (entity && options.storageFactory) {
379
+ const baseStorage = options.storageFactory(entity.entityClass, entity.name);
380
+ const outputSchema = handler.outputOverride ?? (handler.ctor as any).__output;
381
+ const scoped = outputSchema && outputSchema !== entity.entityClass
382
+ ? baseStorage.output(outputSchema)
383
+ : baseStorage;
384
+ // The view is handed over so a filter on a field this door hides is SAID. The
385
+ // guard holds no logger — a warning is the boot's to voice, as a seed's report is.
386
+ const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name, {
387
+ ...(outputSchema && outputSchema !== entity.entityClass
388
+ ? { view: (outputSchema as { getFields(): Fields }).getFields() }
389
+ : {}),
390
+ outOfView: (message) => frondLog.warn(message),
391
+ }).guard(scoped);
392
+ surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
393
+ surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
394
+ }
395
+
396
+ const facadeKey = facadeKeyOf(handler.address, handler.surface);
397
+ buildFacade(entity, handler, surfaceScope, facadeKey);
398
+ frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
399
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
400
+ }
401
+
402
+ // A named surface is closed, so what it contains is a fact worth stating.
403
+ // Saying it at boot is the difference between a rule and a rule you can
404
+ // check: an entity you meant to serve and never wrote a handler for is
405
+ // absent HERE, in one line, instead of being discovered missing later.
406
+ const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface as string))].sort();
407
+ for (const surfaceName of surfaceNames) {
408
+ const served = surfaceHandlers
409
+ .filter((h) => h.surface === surfaceName)
410
+ .map((h) => h.address)
411
+ .sort();
412
+ const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
413
+ frondLog.info(
414
+ `surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
415
+ (absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
416
+ );
417
+ }
418
+
419
+ container.registerValue(`frond:${frond.name}`, scope);
420
+ frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
421
+ }
@@ -1,9 +1,9 @@
1
1
  import { lowerFirst } from '@fougere/schema';
2
- import type { FrondDescriptor, ProviderEntry } from '../descriptor/frond.js';
2
+ import { nameOf, type FrondDescriptor, type ProviderEntry } from '../descriptor/frond.js';
3
3
  import { inheritsCrud } from '../prefab/crud.js';
4
4
  import { targetOf } from '../prefab/prefab.js';
5
5
  import { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
6
- import { entityOfStorageKey } from '../storage.js';
6
+ import { entityOfStorageKey } from '../storage/port.js';
7
7
 
8
8
  /** Who owns an entity's storage. */
9
9
  export function ownersOf(providers: readonly ProviderEntry[]): Map<string, string> {
@@ -53,6 +53,21 @@ export function refuseStorageInUserCode(
53
53
  ];
54
54
  const holders = frond.providers.map((p) => ({ ...p, kind: 'provider' as const }));
55
55
 
56
+ for (const provider of holders) {
57
+ // The key the boot registers it under, which a bundler may have renamed — the same
58
+ // reading `ProviderEntry.name` exists for.
59
+ const registered = nameOf(provider);
60
+ const held = entityOfStorageKey(registered, known);
61
+ if (!held) continue;
62
+
63
+ throw new Error(
64
+ `[storage] ${registered} is a provider, and it is the container key of ${held}'s `
65
+ + `own storage — the storage is registered second, so the provider is never resolved.\n`
66
+ + ` Name it for what it holds; \`<Entity>Storage\` belongs to the entity.\n`
67
+ + ` ${provider.filePath}`,
68
+ );
69
+ }
70
+
56
71
  for (const decl of [...doors, ...holders]) {
57
72
  const allowed = decl.kind === 'provider' ? builtOn(decl.ctor) : [];
58
73
  for (const dep of decl.deps) {
package/src/boot/ports.ts CHANGED
@@ -1,11 +1,20 @@
1
- import type { ProviderEntry } from '../descriptor/frond.js';
1
+ import { nameOf, type ProviderEntry } from '../descriptor/frond.js';
2
2
 
3
- /** A port is a class something already answers under. */
3
+ /**
4
+ * A port is a class something already answers under, and what answers it may be a CHAIN:
5
+ * the realization, wrapped by whoever stands in front of it.
6
+ *
7
+ * A wrapper is recognized by its FORM — it extends the port AND asks for it. Nothing
8
+ * declares it, the way nothing declares a realization.
9
+ *
10
+ * Returns, per port, the chain from the OUTSIDE IN: the last entry is the realization,
11
+ * every one before it wraps what follows.
12
+ */
4
13
  export function portBindings(
5
14
  providers: ProviderEntry[],
6
15
  answers: (name: string) => boolean,
7
- chosen: Record<string, string> | undefined,
8
- ): Map<string, ProviderEntry> {
16
+ chosen: Record<string, string | readonly string[]> | undefined,
17
+ ): Map<string, ProviderEntry[]> {
9
18
  // port class name → the classes that extend it, in scan order.
10
19
  const candidates = new Map<string, ProviderEntry[]>();
11
20
  for (const provider of providers) {
@@ -22,32 +31,67 @@ export function portBindings(
22
31
  candidates.set(port, [...(candidates.get(port) ?? []), provider]);
23
32
  }
24
33
 
25
- const bound = new Map<string, ProviderEntry>();
26
- for (const [port, impls] of candidates) {
27
- const named = chosen?.[port];
28
- if (named) {
29
- const pick = impls.find((i) => i.ctor.name === named);
34
+ const bound = new Map<string, ProviderEntry[]>();
35
+ for (const [port, all] of candidates) {
36
+ /** It extends the port and asks for it: it stands in front of what answers. */
37
+ const wraps = (one: ProviderEntry) => one.deps.includes(port);
38
+ const wrappers = all.filter(wraps);
39
+ const impls = all.filter((one) => !wraps(one));
40
+ const stated = chosen?.[port];
41
+
42
+ bound.set(port, chain(port, impls, wrappers, stated));
43
+ }
44
+
45
+ return bound;
46
+ }
47
+
48
+ function chain(
49
+ port: string,
50
+ impls: ProviderEntry[],
51
+ wrappers: ProviderEntry[],
52
+ stated: string | readonly string[] | undefined,
53
+ ): ProviderEntry[] {
54
+ // The whole chain, outside in — a string is a chain of one, and the last name is what
55
+ // actually answers. What a deployment wraps its realization with is the same kind of
56
+ // decision as which realization it uses, so it is the same key.
57
+ if (stated !== undefined) {
58
+ const order = typeof stated === 'string' ? [stated] : stated;
59
+ const all = [...wrappers, ...impls];
60
+
61
+ return order.map((name) => {
62
+ const pick = all.find((one) => nameOf(one) === name);
30
63
  if (!pick) {
31
64
  throw new Error(
32
- `[ports] ${port}: '${named}' does not extend it. `
33
- + `What does: ${impls.map((i) => i.ctor.name).join(', ')}.`,
65
+ `[ports] ${port}: '${name}' does not extend it. `
66
+ + `What does: ${all.map(nameOf).join(', ')}.`,
34
67
  );
35
68
  }
36
- bound.set(port, pick);
37
- continue;
38
- }
39
- if (impls.length > 1) {
40
- // Refusing rather than keeping one, for the reason `remotes` refuses two owners
41
- // of an entity: whichever won would depend on scan order, and the handler would
42
- // charge the wrong provider without a word.
43
- throw new Error(
44
- `[ports] ${impls.map((i) => i.ctor.name).join(' and ')} both extend ${port}, `
45
- + 'and nothing says which one answers it. Which realization a deployment uses is '
46
- + `not a fact about the code — state it: ports: { ${port}: '${impls[0].ctor.name}' } `
47
- + 'in fougere.config.ts.',
48
- );
49
- }
50
- bound.set(port, impls[0]);
69
+
70
+ return pick;
71
+ });
51
72
  }
52
- return bound;
73
+
74
+ if (impls.length > 1) {
75
+ // Refusing rather than keeping one, for the reason `remotes` refuses two owners
76
+ // of an entity: whichever won would depend on scan order, and the handler would
77
+ // charge the wrong provider without a word.
78
+ throw new Error(
79
+ `[ports] ${impls.map(nameOf).join(' and ')} both extend ${port}, `
80
+ + 'and nothing says which one answers it. Which realization a deployment uses is '
81
+ + `not a fact about the code — state it: ports: { ${port}: '${nameOf(impls[0]!)}' } `
82
+ + 'in fougere.config.ts.',
83
+ );
84
+ }
85
+
86
+ if (wrappers.length > 1) {
87
+ // Same refusal one layer out: two wrappers are an ORDER, and scan order is not one.
88
+ throw new Error(
89
+ `[ports] ${wrappers.map(nameOf).join(' and ')} both wrap ${port}, `
90
+ + 'and nothing says which stands in front. State the chain, outside in: '
91
+ + `ports: { ${port}: [${[...wrappers, ...impls].map((one) => `'${nameOf(one)}'`).join(', ')}] } `
92
+ + 'in fougere.config.ts.',
93
+ );
94
+ }
95
+
96
+ return [...wrappers, ...impls];
53
97
  }
@@ -2,7 +2,7 @@
2
2
  import type { FrondCall, Transport } from '../wire/call.js';
3
3
  import { assertIdentityCard, RPC_ENTITY } from '../wire/call.js';
4
4
  import { runMiddlewares, type AppMiddleware, type OperationContext } from '../wire/middleware.js';
5
- import { EMPTY_INVOCATION, type InvocationContext } from '../wire/Invocation.js';
5
+ import { type InvocationContext, Invocation } from '../wire/Invocation.js';
6
6
  import { FougereError, ErrorCode } from '../wire/errors.js';
7
7
  import { Card, type SchemaView, type SchemaDescriptor } from '@fougere/schema';
8
8
  import { dynamicOperations } from '../entry/facade.js';
@@ -43,7 +43,7 @@ export function createRemoteRouter(
43
43
  const transport = transports.get(url) ?? makeTransport(url);
44
44
  transports.set(url, transport);
45
45
  try {
46
- const answer = await transport({ entity: RPC_ENTITY, op: 'discover' }, EMPTY_INVOCATION);
46
+ const answer = await transport({ entity: RPC_ENTITY, op: 'discover' }, Invocation.empty);
47
47
  // Judged below and not here: this catch means "unreachable, retry", and a
48
48
  // refusal thrown inside it would be swallowed into another silent retry.
49
49
  return { label, url, transport, answer };
@@ -118,7 +118,7 @@ export function createRemoteFacade(
118
118
  router: RemoteRouter,
119
119
  middlewaresFor: (address: string) => AppMiddleware[],
120
120
  ): Facade {
121
- const opFn = (op: string) => async (invocation: InvocationContext = EMPTY_INVOCATION) => {
121
+ const opFn = (op: string) => async (invocation: InvocationContext = Invocation.empty) => {
122
122
  const { frond, transport } = await router.route(entity);
123
123
  const call: FrondCall = { frond, entity, op };
124
124
  const ctx: OperationContext = {