@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/seed.ts CHANGED
@@ -4,8 +4,15 @@ import type { FrondDescriptor, SeedEntry, SeedFactory } from '../descriptor/fron
4
4
  import type { App } from './types.js';
5
5
  import type { Extension } from './AppLifecycle.js';
6
6
 
7
+ /** What the ordering satisfied, and what no order can. */
8
+ export interface SeedOrder {
9
+ ordered: SeedEntry[];
10
+ /** Seeds whose `ref()` targets wait on each other — no order plants them all. */
11
+ cycle: SeedEntry[];
12
+ }
13
+
7
14
  /** Seeds in dependency order — a `ref()` target is planted before its referrer. */
8
- export function orderSeeds(fronds: FrondDescriptor[]): SeedEntry[] {
15
+ export function orderSeeds(fronds: FrondDescriptor[]): SeedOrder {
9
16
  const refs = new Map<string, Set<string>>();
10
17
  for (const frond of fronds) {
11
18
  for (const entity of frond.entities) {
@@ -45,7 +52,7 @@ export function orderSeeds(fronds: FrondDescriptor[]): SeedEntry[] {
45
52
  waiting.delete(ready);
46
53
  }
47
54
 
48
- return [...ordered, ...waiting.keys()];
55
+ return { ordered, cycle: [...waiting.keys()] };
49
56
  }
50
57
 
51
58
  /** Where a seed writes, and what it may skip — resolved per entity. */
@@ -120,7 +127,15 @@ export function seeding(report?: (message: string) => void): Extension {
120
127
  return {
121
128
  name: 'seeds',
122
129
  up: async (app: App) => {
123
- const seeds = orderSeeds(app.fronds);
130
+ const { ordered, cycle } = orderSeeds(app.fronds);
131
+ if (cycle.length > 0) {
132
+ report?.(
133
+ ` ${cycle.map((seed) => seed.entityName).join(', ')}: a ref() cycle, and no order `
134
+ + 'plants them all — planted in the order they are declared, and the source answers for the rest',
135
+ );
136
+ }
137
+
138
+ const seeds = [...ordered, ...cycle];
124
139
  if (seeds.length > 0) await runSeeds(app, seeds, report);
125
140
  },
126
141
  };
@@ -1,6 +1,6 @@
1
1
  /** Where a STATED contract and the signature it is about have stopped agreeing. */
2
2
  import type { FrondDescriptor, HandlerEntry } from '../descriptor/frond.js';
3
- import type { ScanDiagnostic } from '../scan/result.js';
3
+ import type { ScanDiagnostic } from '../scan.js';
4
4
  import type { BindingPlan } from '../wire/binding.js';
5
5
  import type { Signature } from '../wire/signature.js';
6
6
 
@@ -2,7 +2,7 @@
2
2
  import { ambient } from '#ambient';
3
3
  import { upperFirst, lowerFirst, type SchemaView } from '@fougere/schema';
4
4
  import type { Container } from '@fougere/container';
5
- import { membersOfTogetherKey, storageKeyOf, entityOfStorageKey, type Storage, type StorageFactory } from '../storage.js';
5
+ import { membersOfTogetherKey, storageKeyOf, entityOfStorageKey, type Storage, type StorageFactory } from '../storage/port.js';
6
6
  import type { Logger } from '../builtin/logger.js';
7
7
  import type { ProviderEntry } from '../descriptor/frond.js';
8
8
  import { StorageGuard } from '../dispatch/StorageGuard.js';
package/src/boot/types.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  import type { Container } from '@fougere/container';
3
3
  import type { Fronds } from '../descriptor/Fronds.js';
4
4
  import type { FrondDescriptor } from '../descriptor/frond.js';
5
- import type { ScanResult } from '../scan/result.js';
5
+ import type { ScanResult } from '../scan.js';
6
6
  import type { SchemaView } from '@fougere/schema';
7
- import type { StorageFactory } from '../storage.js';
7
+ import type { StorageFactory } from '../storage/port.js';
8
8
  import type { Constraint } from '../source.js';
9
9
  import type { AppMiddleware } from '../wire/middleware.js';
10
10
  import type { RpcAnswer, Transport } from '../wire/call.js';
@@ -28,6 +28,16 @@ export interface CreateAppOptions {
28
28
  sourceOf?: (entityName: string) => string;
29
29
  transacts?: (source: string) => boolean;
30
30
  transacted?: <R>(source: string, fn: (storageFactory: StorageFactory) => Promise<R>) => Promise<R>;
31
+ /**
32
+ * Bring the shape of what lives here up to date — a source's own gesture, declared on
33
+ * `Source.migrate` and handed over whole.
34
+ *
35
+ * Handed here rather than assembled by the host: four of them wrote
36
+ * `migrating(storage.migrate)` themselves, one of them as a string inside generated
37
+ * code, and eight demos wrote nothing — so they had no migration and nothing said it.
38
+ * The ASCENT is core's to order, since rows after tables is not a host's preference.
39
+ */
40
+ migrate?: (app: App) => Promise<void> | void;
31
41
  /** Whether that source refuses a constraint at the rows — the boot says so when it does not. */
32
42
  enforces?: (source: string, constraint: Constraint) => boolean;
33
43
  /** Builds the cross-source reader a frond gets when it declares `reads:`. */
@@ -47,7 +57,12 @@ export interface CreateAppOptions {
47
57
  * Which realization answers which port — see `FougereConfig.ports`. Needed only
48
58
  * when two classes extend the same port; one is resolved by convention.
49
59
  */
50
- ports?: Record<string, string>;
60
+ /**
61
+ * What answers a port. A string names the realization; a LIST is the chain, from the
62
+ * outside in — `['Retrying', 'Stripe']` puts `Retrying` in front of `Stripe`, and the
63
+ * last name is what actually charges.
64
+ */
65
+ ports?: Record<string, string | readonly string[]>;
51
66
  /** What this app takes on beyond its fronds, each stating what it does and what it undoes. */
52
67
  extensions?: readonly (Extension | undefined)[];
53
68
  /**
@@ -0,0 +1,38 @@
1
+ import { entity, oneOf, text, json, list, optional, created } from '@fougere/schema';
2
+
3
+ /**
4
+ * One line. A fact about the process, announced like any other — which is what makes a
5
+ * destination an ordinary handler and `remotes:` the only thing that decides where it runs.
6
+ *
7
+ * Here and not in `@fougere/log`, because the BOOT announces: core would otherwise have to
8
+ * name an optional package's entity to reach its own lines. What is optional is where a
9
+ * line GOES, and that is what `@fougere/log` ships.
10
+ *
11
+ * No `primary()`: an id costs 87 µs to generate (cuid2), and nothing addresses a line
12
+ * by one. `at` is `created()`, so announcing is what fills it.
13
+ */
14
+ export default class LogLine extends entity({
15
+ level: oneOf('debug', 'info', 'warn', 'error'),
16
+ /** Who wrote it — 'app', 'app:catalog'. */
17
+ name: text(),
18
+ message: text({ min: 1 }),
19
+ /** What the writer passed beside the message — most lines pass none. */
20
+ args: optional(list(json())),
21
+ at: created(),
22
+ }) {}
23
+
24
+ /** The fact a boot announces, spelled once. */
25
+ export const LOG_LINE = 'logLine';
26
+
27
+ /**
28
+ * The addresses whose operations CARRY a line — every destination this app installed.
29
+ *
30
+ * Whatever carries a fact must not produce one, or the emission refuses it by name:
31
+ * keeping a line is a DISPATCH, so logging it announces a line inside the announcement of
32
+ * one. FILLED BY THE BOOT from what binds `logLine`, not written down — a hard-coded list
33
+ * cannot know a third party's destination, and the app already knows who subscribed.
34
+ *
35
+ * Read by `loggerMiddleware` and by `observability`'s `trace()`, both of which observe
36
+ * every operation and would otherwise observe the observation.
37
+ */
38
+ export const CARRIES_LINE = new Set<string>();
@@ -18,6 +18,54 @@ export type LogSink = (record: LogRecord) => void;
18
18
  /** Who else takes this process's log lines, beside the console. */
19
19
  const sinks: LogSink[] = [];
20
20
 
21
+ /**
22
+ * Where one boot's lines wait, and where they go once they can.
23
+ *
24
+ * PER BOOT and never per process: two apps in one process each have their own
25
+ * destinations, and a slot shared between them sent the second app's lines to the first
26
+ * app's door — measured on `demos/observability`, where only the first of three printed.
27
+ *
28
+ * A boot writes most of what a process ever logs, and it writes it before any emission is
29
+ * registered, so the lines that say what an app is made of are the ones a destination
30
+ * would miss. Bounded: a boot that never finishes must not grow, and what is dropped is
31
+ * the OLDEST since the lines explaining a refusal are the last.
32
+ */
33
+ export class Carry {
34
+ private held: LogRecord[] = [];
35
+ private take?: (line: LogRecord) => void;
36
+
37
+ static readonly MAX = 500;
38
+
39
+ /** Take it now, or keep it for whoever arrives. */
40
+ push(record: LogRecord): void {
41
+ if (this.take) {
42
+ try {
43
+ this.take(record);
44
+ } catch { /* announcing never breaks logging, for the same reason a sink does not */ }
45
+ return;
46
+ }
47
+ this.held.push(record);
48
+ if (this.held.length > Carry.MAX) this.held.shift();
49
+ }
50
+
51
+ /** Hand what is held over, and everything after it. */
52
+ to(take: (line: LogRecord) => void): () => void {
53
+ this.take = take;
54
+ for (const line of this.held.splice(0)) take(line);
55
+
56
+ return () => { this.take = undefined; };
57
+ }
58
+
59
+ /**
60
+ * Forget what is still held — a boot that refused, or an app that declares no
61
+ * destination. Nothing is printed: the console had every one of these lines when it was
62
+ * written, and the hold exists only to hand them on later.
63
+ */
64
+ forget(): void {
65
+ this.held.length = 0;
66
+ }
67
+ }
68
+
21
69
  /** Take every line this process logs. Returns the way to withdraw. */
22
70
  export function onLog(next: LogSink): () => void {
23
71
  sinks.push(next);
@@ -79,8 +127,8 @@ function supportsColor(): boolean {
79
127
  return false;
80
128
  }
81
129
 
82
- function formatTime(): string {
83
- const d = new Date();
130
+ function stamp(at: number | Date): string {
131
+ const d = new Date(at);
84
132
  const h = String(d.getHours()).padStart(2, '0');
85
133
  const m = String(d.getMinutes()).padStart(2, '0');
86
134
  const s = String(d.getSeconds()).padStart(2, '0');
@@ -91,6 +139,12 @@ function formatTime(): string {
91
139
  export interface LoggerOptions {
92
140
  /** Logger name / prefix. */
93
141
  name?: string;
142
+ /**
143
+ * Where its lines go — one boot's, so two apps in a process do not share a door. A
144
+ * logger without one writes to the console and nowhere else, which is what the boot's
145
+ * first lines do and what an app declaring no destination does forever.
146
+ */
147
+ carry?: Carry;
94
148
  /** Force color on/off. Auto-detected by default. */
95
149
  color?: boolean;
96
150
  }
@@ -98,15 +152,17 @@ export interface LoggerOptions {
98
152
  export class Logger {
99
153
  private name: string;
100
154
  private color: boolean;
155
+ private carry?: Carry;
101
156
 
102
157
  constructor(prefix?: string, options?: Omit<LoggerOptions, 'name'>) {
103
158
  this.name = prefix ?? 'app';
104
159
  this.color = options?.color ?? supportsColor();
160
+ this.carry = options?.carry;
105
161
  }
106
162
 
107
163
  /** Create a child logger with a sub-name. It carries no level of its own either. */
108
164
  child(name: string): Logger {
109
- return new Logger(`${this.name}:${name}`, { color: this.color });
165
+ return new Logger(`${this.name}:${name}`, { color: this.color, ...(this.carry ? { carry: this.carry } : {}) });
110
166
  }
111
167
 
112
168
  debug(msg: string, ...args: unknown[]) { this.log('debug', msg, args); }
@@ -116,27 +172,63 @@ export class Logger {
116
172
 
117
173
  private log(level: string, msg: string, args: unknown[]) {
118
174
  if (LEVELS[level as LogLevel] < threshold) return;
175
+ const record: LogRecord = {
176
+ level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now(),
177
+ };
119
178
 
120
179
  // Beside the console, never instead of it: a forwarded line is an addition, and a
121
180
  // sink that throws must not cost the operator the line they were reading.
122
181
  for (const take of sinks) {
123
182
  try {
124
- take({ level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now() });
183
+ take(record);
125
184
  } catch { /* forwarding never breaks logging */ }
126
185
  }
127
186
 
128
- const style = LEVEL_STYLE[level];
129
- const time = formatTime();
130
- // One console method per level. `debug` and `info` both went to `console.log`, so
131
- // nothing downstream — a terminal filter, a collector — could tell them apart.
132
- const method = level as 'debug' | 'info' | 'warn' | 'error';
133
-
134
- if (this.color) {
135
- const c = COLORS[style.color];
136
- const prefix = `${COLORS.dim}${time}${COLORS.reset} ${c}${COLORS.bold}${style.badge}${COLORS.reset} ${COLORS.magenta}${this.name}${COLORS.reset}`;
137
- console[method](prefix, msg, ...args);
138
- } else {
139
- console[method](`${time} ${style.badge} [${this.name}]`, msg, ...args);
140
- }
187
+ this.carry?.push(record);
188
+
189
+ // The console ALWAYS, whoever else took the line. Skipping it once a destination
190
+ // existed made `calls()` — a devtools ring that prints nothing — silence the
191
+ // operator's terminal: 306 per-operation lines in `demos/observability` became 2.
192
+ // A destination sends a line ELSEWHERE; it does not take over stderr.
193
+ const { method, text } = formatted(record, this.color);
194
+ console[method](...text);
141
195
  }
142
196
  }
197
+
198
+ /**
199
+ * One line, ready for a terminal — the console arguments and which method takes them.
200
+ *
201
+ * Here rather than inside the class because the boot is not the only writer: a destination
202
+ * that prints (`@fougere/log`) hands its own record to the same formatting, so the two
203
+ * outputs cannot drift.
204
+ *
205
+ * One console method per level: `debug` and `info` both went to `console.log`, so nothing
206
+ * downstream — a terminal filter, a collector — could tell them apart.
207
+ */
208
+ export interface Rendered {
209
+ level: Exclude<LogLevel, 'silent'>;
210
+ name: string;
211
+ message: string;
212
+ /** Absent on most lines: a message usually carries its own detail. */
213
+ args?: unknown[] | null;
214
+ /** Epoch milliseconds from a logger, a `Date` from an entity that stamped it. */
215
+ at: number | Date;
216
+ }
217
+
218
+ export function formatted(
219
+ record: Rendered,
220
+ color = supportsColor(),
221
+ ): { method: Rendered['level']; text: unknown[] } {
222
+ const style = LEVEL_STYLE[record.level];
223
+ const time = stamp(record.at);
224
+ const method = record.level;
225
+
226
+ if (color) {
227
+ const c = COLORS[style.color];
228
+ const prefix = `${COLORS.dim}${time}${COLORS.reset} ${c}${COLORS.bold}${style.badge}${COLORS.reset} ${COLORS.magenta}${record.name}${COLORS.reset}`;
229
+
230
+ return { method, text: [prefix, record.message, ...(record.args ?? [])] };
231
+ }
232
+
233
+ return { method, text: [`${time} ${style.badge} [${record.name}]`, record.message, ...(record.args ?? [])] };
234
+ }
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import type { AuthConfig } from './boot/auth.js';
4
4
  import type { LogLevel } from './builtin/logger.js';
5
- import type { ConventionsInput } from './scan/conventions.js';
5
+ import type { ConventionsInput } from './conventions.js';
6
6
  import { getModuleLoader } from './loader.js';
7
7
 
8
8
  // ── Types ────────────────────────────────────────
@@ -22,7 +22,8 @@ export interface FougereConfig {
22
22
  /** Remote fronds — frondName → base URL. */
23
23
  remotes?: Record<string, string>;
24
24
  /** Which realization answers which port — port class name → implementation class name. */
25
- ports?: Record<string, string>;
25
+ /** What answers a port — a name, or the chain from the outside in. */
26
+ ports?: Record<string, string | readonly string[]>;
26
27
  /** Auth declaration — picks a provider package and forwards options to it. */
27
28
  auth?: AuthConfig;
28
29
  /** Which protocol adapters this app serves. */
package/src/contract.ts CHANGED
@@ -3,25 +3,21 @@
3
3
  * runtime: the error vocabulary, the invocation shape, the call value, the naming convention.
4
4
  */
5
5
  import { lowerFirst } from '@fougere/schema';
6
- import { canonicalInvocation, type InvocationContext } from './wire/Invocation.js';
6
+ import { Invocation, type InvocationContext } from './wire/Invocation.js';
7
7
  import type { FrondCall } from './wire/call.js';
8
8
 
9
9
  export { FougereError, ErrorCode, validationErrorsOf } from './wire/errors.js';
10
10
  // What a read may ask about one field. Here rather than on the main entry because an
11
11
  // adapter reads it to compile a query, and an adapter carries no boot.
12
- export { comparisonOf, comparisonsIn, unknownIn, COMPARISONS } from './criterion.js';
13
- export type { Comparison, ComparisonName } from './criterion.js';
12
+ export { comparisonOf, comparisonsIn } from './storage/criterion.js';
13
+ export type { Comparison } from './storage/criterion.js';
14
14
  // A receiver turns a refusal into what may cross a process boundary, and it is not
15
15
  // core's boot — reaching it through the main entry dragged the scanner into a bundle.
16
16
  export { toPublicError } from './wire/http-error.js';
17
- export type { FougereErrorOptions } from './wire/errors.js';
18
- export { canonicalInvocation, EMPTY_INVOCATION } from './wire/Invocation.js';
19
- export type { InvocationContext } from './wire/Invocation.js';
20
17
  export { Invocation } from './wire/Invocation.js';
21
- export type { PartialInvocation } from './wire/Invocation.js';
18
+ export type { InvocationContext } from './wire/Invocation.js';
22
19
  export { Call } from './wire/call.js';
23
20
  export { RouteAddress } from './wire/RouteAddress.js';
24
- export type { RouteAddressInput } from './wire/RouteAddress.js';
25
21
  export type { FrondCall, Transport, SignedCall } from './wire/call.js';
26
22
  export { MAX_BODY_BYTES } from './wire/call.js';
27
23
  // The reserved entity, VALUE and not type: a consumer that wants to leave it alone — a
@@ -59,5 +55,5 @@ export function callValueOf(
59
55
  typeof opOrInput === 'string'
60
56
  ? [{ entity: lowerFirst((target as { name: string }).name), op: opOrInput }, input]
61
57
  : [target as FrondCall, opOrInput];
62
- return { call, invocation: canonicalInvocation(given) };
58
+ return { call, invocation: Invocation.from(given) };
63
59
  }
@@ -14,6 +14,12 @@ export interface Conventions {
14
14
  presenters: string;
15
15
  collectors: string;
16
16
  seeds: string;
17
+ /**
18
+ * What runs around this frond's operations. Scoped to the frond that declares it,
19
+ * unless `frond.config.ts` widens it — the one directory whose members apply to code
20
+ * they do not name.
21
+ */
22
+ middlewares: string;
17
23
  /**
18
24
  * What the domain computes with nothing injected. Read into the type program, because
19
25
  * a handler names one of its types in a signature, and registered nowhere: no class
@@ -37,6 +43,7 @@ export const DEFAULT_CONVENTIONS: Conventions = {
37
43
  presenters: 'presenters',
38
44
  collectors: 'collectors',
39
45
  seeds: 'seeds',
46
+ middlewares: 'middlewares',
40
47
  rules: 'rules',
41
48
  versions: 'versions',
42
49
  },
@@ -70,9 +77,9 @@ export function providerDirsOf(conventions: Conventions): string[] {
70
77
 
71
78
  /** The frond vocabulary — every directory the scan reads. */
72
79
  export function frondDirsOf(conventions: Conventions): string[] {
73
- const { entities, handlers, presenters, collectors, seeds, rules, versions } = conventions.dirs;
80
+ const { entities, handlers, presenters, collectors, seeds, middlewares, rules, versions } = conventions.dirs;
74
81
  return [...new Set([
75
- entities, handlers, presenters, collectors, seeds, rules, versions,
82
+ entities, handlers, presenters, collectors, seeds, middlewares, rules, versions,
76
83
  ...providerDirsOf(conventions),
77
84
  ])];
78
85
  }
@@ -24,13 +24,6 @@ export function unb64url(input: string): Uint8Array {
24
24
  }
25
25
 
26
26
  /** Plain base64, padded — what an environment variable carries a PEM as. */
27
- export function b64(input: Uint8Array | string): string {
28
- const bytes = typeof input === 'string' ? bytesOf(input) : input;
29
- let binary = '';
30
- for (const byte of bytes) binary += String.fromCharCode(byte);
31
- return btoa(binary);
32
- }
33
-
34
27
  export const unb64 = (input: string): Uint8Array => {
35
28
  const binary = atob(input);
36
29
  const bytes = new Uint8Array(binary.length);
package/src/declare.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /** A frond stated by its author, for an app that will not scan. */
2
2
  import { lowerFirst, type SchemaView } from '@fougere/schema';
3
3
  import type {
4
- CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry,
4
+ CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry, MiddlewareEntry,
5
5
  PresenterEntry, ProviderEntry, SeedEntry,
6
6
  } from './descriptor/frond.js';
7
- import { DEFAULT_CONVENTIONS } from './scan/conventions.js';
7
+ import { DEFAULT_CONVENTIONS } from './conventions.js';
8
8
  import { getPresenterFields } from './prefab/presenter.js';
9
9
  import type { OperationContract } from './wire/operation.js';
10
10
 
@@ -90,6 +90,14 @@ export interface FrondDeclaration {
90
90
  collectors?: (Ctor | DeclaredSubject)[];
91
91
  providers?: Declared[];
92
92
  seeds?: { entityName: string; data: SeedEntry['data'] }[];
93
+ /**
94
+ * What runs around this frond's operations. The scope is stated beside the class here,
95
+ * where `frond.config.ts` states it by class name — a statement has no config file to
96
+ * put it in.
97
+ */
98
+ middlewares?: (Ctor | (DeclaredSubject & { scope?: MiddlewareEntry['scope'] }))[];
99
+ /** The ops that finish a fact, in order — the same key `frond.config.ts` states. */
100
+ pipes?: Record<string, string[]>;
93
101
  /** Per-surface entity lists — the same key `frond.config.ts` states. */
94
102
  surfaces?: Record<string, string[]>;
95
103
  /** The import scope this frond answers under. Defaults to the conventional one. */
@@ -179,6 +187,14 @@ export function frond(name: string, declared: FrondDeclaration = {}): FrondDescr
179
187
  filePath: '',
180
188
  }));
181
189
 
190
+ const middlewares: MiddlewareEntry[] = (declared.middlewares ?? []).map((m) => ({
191
+ name: ctorOf(m).name,
192
+ ctor: ctorOf(m),
193
+ scope: (typeof m === 'function' ? undefined : m.scope) ?? 'frond',
194
+ deps: depsOf(m),
195
+ filePath: '',
196
+ }));
197
+
182
198
  return {
183
199
  name,
184
200
  source: { path: '', package: `${scope}/${name}` },
@@ -189,6 +205,8 @@ export function frond(name: string, declared: FrondDeclaration = {}): FrondDescr
189
205
  presenters,
190
206
  collectors,
191
207
  seeds,
208
+ middlewares,
209
+ ...(declared.pipes ? { pipes: declared.pipes } : {}),
192
210
  ...(declared.surfaces ? { surfaces: declared.surfaces } : {}),
193
211
  };
194
212
  }
@@ -112,6 +112,23 @@ export interface CollectorEntry {
112
112
  filePath: string;
113
113
  }
114
114
 
115
+ /**
116
+ * A discovered middleware — a class declaring `around(ctx, next)`, which runs before and
117
+ * after every operation in its scope.
118
+ */
119
+ export interface MiddlewareEntry {
120
+ /** Class name — what `frond.config.ts` addresses to widen the scope. */
121
+ name: string;
122
+ /** The middleware class. */
123
+ ctor: new (...args: never[]) => unknown;
124
+ /** How far it reaches: its own frond's entities, or every operation in the process. */
125
+ scope: 'frond' | 'app';
126
+ /** Constructor dependency type names (from AST scan). */
127
+ deps: string[];
128
+ /** Absolute file path (for debugging). */
129
+ filePath: string;
130
+ }
131
+
115
132
  /** A discovered seed file (array of records or async factory). */
116
133
  export interface SeedEntry {
117
134
  /** Entity name this seed targets (from filename: Author.seed.ts → 'author'). */
@@ -142,6 +159,16 @@ export interface FrondDescriptor {
142
159
  presenters: PresenterEntry[];
143
160
  collectors: CollectorEntry[];
144
161
  seeds: SeedEntry[];
162
+ middlewares: MiddlewareEntry[];
163
+ /**
164
+ * Brought by an EXTENSION rather than by the app — `@fougere/calls` keeping its lines,
165
+ * `@fougere/observability` sending them on. It is installed like any other, and it is not
166
+ * what the app SERVES: a report that lists it describes the instrumentation, not the
167
+ * domain. Set by the boot, never by a declaration.
168
+ */
169
+ brought?: true;
170
+ /** The ops that finish a fact, in order — see `FrondConfig.pipes`. */
171
+ pipes?: Record<string, string[]>;
145
172
  /**
146
173
  * Per-surface entity lists from frond.config.ts (e.g. { graphql: ['Post'], rest: ['Post',
147
174
  * 'Author'] }).
@@ -156,5 +183,9 @@ export interface FrondDescriptor {
156
183
  handlerName?: string;
157
184
  /** Method name on the resolved handler (defaults to op name). */
158
185
  method?: string;
186
+ /** The GraphQL root field this op answers to — read by `adapter/graphql`. */
187
+ graphql?: string;
188
+ /** Where it answers over REST — read by `adapter/rest`, the dual of `graphql`. */
189
+ rest?: { method?: string; path?: string; status?: number };
159
190
  }>;
160
191
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What it takes to PRODUCE a descriptor — the surface a scan reads, whoever wrote it.
3
+ * The main entry serves what an app RUNS; this one serves what fills it in.
4
+ */
5
+ export * from './frond.js';
6
+ export { Fronds } from './Fronds.js';
7
+
8
+ export { cardinalityOf, type OperationContract, type OperationsMap, type Param, type TypeRef } from '../wire/operation.js';
9
+ export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
10
+ export type { Signature } from '../wire/signature.js';
11
+ export { emitKeyOf, awaitKeyOf } from '../wire/emit.js';
12
+ export { storageKeyOf } from '../storage/port.js';
13
+ export { getPresenterFields } from '../prefab/presenter.js';
14
+ export { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
15
+ export { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
@@ -0,0 +1,23 @@
1
+ import type { FrondDescriptor, HandlerEntry } from './frond.js';
2
+
3
+ /**
4
+ * The surfaces a handler answers on — its own when it has one, otherwise the default
5
+ * and every surface that names its address without opening a door of its own.
6
+ * `undefined` is the default surface, as `facadeKeyOf` and `RouteAddress` spell it.
7
+ */
8
+ export function servedSurfaces(
9
+ frond: FrondDescriptor,
10
+ handler: HandlerEntry,
11
+ ): (string | undefined)[] {
12
+ if (handler.surface) return [handler.surface];
13
+
14
+ const declared = Object.entries(frond.surfaces ?? {})
15
+ .filter(([surface, addresses]) =>
16
+ addresses.some((address) => address.toLowerCase() === handler.address.toLowerCase())
17
+ && !frond.handlers.some((other) =>
18
+ other.address === handler.address && other.surface === surface))
19
+ .map(([surface]) => surface)
20
+ .sort();
21
+
22
+ return [undefined, ...declared];
23
+ }
@@ -38,8 +38,12 @@ export class ArgumentResolver {
38
38
  args.push(val);
39
39
  break;
40
40
  }
41
- case 'fact': {
42
- // A fact IS the payload — the whole of what happened, never a piece of it.
41
+ case 'fact':
42
+ case 'pipe': {
43
+ // A fact IS the payload — the whole of what happened, never a piece of it. The
44
+ // same holds before it is final: an op that finishes one is handed all of it,
45
+ // and answers all of it.
46
+ //
43
47
  // Identical to `input` today, and deliberately not sharing its branch: the two
44
48
  // agree by coincidence, not by rule, and the day `input` learns to look up a
45
49
  // value by parameter name a subscriber would receive ONE FIELD of the fact it
@@ -12,12 +12,13 @@ import { presenterKeyOf } from '../prefab/presenter.js';
12
12
  import { repositoryKeyOf } from '../prefab/repository.js';
13
13
  import type { OperationContract, OperationsMap } from '../wire/operation.js';
14
14
  import type { EffectiveOperation, EffectiveOperationsMap } from '../effective-operation.js';
15
- import { canonicalInvocation, type InvocationContext } from '../wire/Invocation.js';
15
+ import type { BindingPlan } from '../wire/binding.js';
16
+ import { Invocation, type InvocationContext } from '../wire/Invocation.js';
16
17
  import type { HandlerEntry, PresenterEntry } from '../descriptor/frond.js';
17
18
  import { ArgumentResolver } from './ArgumentResolver.js';
18
19
  import { OutputView } from './OutputView.js';
19
20
  import { PresenterExecutor } from './PresenterExecutor.js';
20
- import { presenterArguments } from './presenterArguments.js';
21
+ import { presenterArguments, presenterPlans } from './presenterArguments.js';
21
22
  import { validateInput } from './validateInput.js';
22
23
 
23
24
  /** What boot resolved around one handler, beyond the handler and the scope it resolves in. */
@@ -54,6 +55,8 @@ export class HandlerFacade {
54
55
  private readonly arguments = new ArgumentResolver(
55
56
  (typeName) => this.collectorResolver(typeName),
56
57
  );
58
+ /** A computed field's parameters and the collectors in scope are both boot-time facts. */
59
+ private readonly presenterPlans: Map<string, BindingPlan>;
57
60
 
58
61
  constructor(
59
62
  private readonly handler: HandlerEntry,
@@ -62,6 +65,10 @@ export class HandlerFacade {
62
65
  ) {
63
66
  this.refuseCrudWithoutRepository(handler);
64
67
 
68
+ this.presenterPlans = door.presenter
69
+ ? presenterPlans(door.presenter, door.collectors)
70
+ : new Map();
71
+
65
72
  scope.register(this.handlerKey, handler.ctor, { deps: this.depsOf(handler) });
66
73
 
67
74
  this.effectiveOperations = door.operations;
@@ -94,7 +101,7 @@ export class HandlerFacade {
94
101
  );
95
102
  }
96
103
 
97
- const invocation = canonicalInvocation(input);
104
+ const invocation = Invocation.from(input);
98
105
  const context: OperationContext = {
99
106
  entity,
100
107
  frond: this.door.frond,
@@ -135,7 +142,7 @@ export class HandlerFacade {
135
142
  op,
136
143
  );
137
144
 
138
- return presenterArguments(presenter, invocation, this.arguments, this.door.collectors)
145
+ return presenterArguments(this.presenterPlans, invocation, this.arguments)
139
146
  .then((args) => executor.present(output, args));
140
147
  }
141
148