@fougere/core 0.8.4-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 (112) hide show
  1. package/dist/boot/AppLifecycle.d.ts +8 -0
  2. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  3. package/dist/boot/AppLifecycle.js.map +1 -1
  4. package/dist/boot/Emissions.d.ts +26 -0
  5. package/dist/boot/Emissions.d.ts.map +1 -1
  6. package/dist/boot/Emissions.js +143 -8
  7. package/dist/boot/Emissions.js.map +1 -1
  8. package/dist/boot/bootstrap.d.ts.map +1 -1
  9. package/dist/boot/bootstrap.js +77 -18
  10. package/dist/boot/bootstrap.js.map +1 -1
  11. package/dist/boot/hosted.d.ts.map +1 -1
  12. package/dist/boot/hosted.js +1 -0
  13. package/dist/boot/hosted.js.map +1 -1
  14. package/dist/boot/install.d.ts +2 -0
  15. package/dist/boot/install.d.ts.map +1 -1
  16. package/dist/boot/install.js +33 -4
  17. package/dist/boot/install.js.map +1 -1
  18. package/dist/boot/ports.d.ts +12 -3
  19. package/dist/boot/ports.d.ts.map +1 -1
  20. package/dist/boot/ports.js +51 -21
  21. package/dist/boot/ports.js.map +1 -1
  22. package/dist/boot/remote.d.ts.map +1 -1
  23. package/dist/boot/remote.js +3 -3
  24. package/dist/boot/remote.js.map +1 -1
  25. package/dist/boot/types.d.ts +16 -1
  26. package/dist/boot/types.d.ts.map +1 -1
  27. package/dist/builtin/LogLine.d.ts +38 -0
  28. package/dist/builtin/LogLine.d.ts.map +1 -0
  29. package/dist/builtin/LogLine.js +37 -0
  30. package/dist/builtin/LogLine.js.map +1 -0
  31. package/dist/builtin/logger.d.ts +57 -0
  32. package/dist/builtin/logger.d.ts.map +1 -1
  33. package/dist/builtin/logger.js +72 -17
  34. package/dist/builtin/logger.js.map +1 -1
  35. package/dist/config-loader.d.ts +2 -1
  36. package/dist/config-loader.d.ts.map +1 -1
  37. package/dist/config-loader.js.map +1 -1
  38. package/dist/contract.d.ts +3 -7
  39. package/dist/contract.d.ts.map +1 -1
  40. package/dist/contract.js +3 -4
  41. package/dist/contract.js.map +1 -1
  42. package/dist/conventions.d.ts +6 -0
  43. package/dist/conventions.d.ts.map +1 -1
  44. package/dist/conventions.js +3 -2
  45. package/dist/conventions.js.map +1 -1
  46. package/dist/declare.d.ts +11 -1
  47. package/dist/declare.d.ts.map +1 -1
  48. package/dist/declare.js +9 -0
  49. package/dist/declare.js.map +1 -1
  50. package/dist/descriptor/frond.d.ts +34 -0
  51. package/dist/descriptor/frond.d.ts.map +1 -1
  52. package/dist/descriptor/index.d.ts +1 -1
  53. package/dist/descriptor/index.d.ts.map +1 -1
  54. package/dist/descriptor/index.js +1 -1
  55. package/dist/descriptor/index.js.map +1 -1
  56. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  57. package/dist/dispatch/ArgumentResolver.js +6 -2
  58. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  59. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  60. package/dist/dispatch/HandlerFacade.js +2 -2
  61. package/dist/dispatch/HandlerFacade.js.map +1 -1
  62. package/dist/effective-operation.d.ts.map +1 -1
  63. package/dist/effective-operation.js +11 -7
  64. package/dist/effective-operation.js.map +1 -1
  65. package/dist/frond-config.d.ts +24 -0
  66. package/dist/frond-config.d.ts.map +1 -1
  67. package/dist/frond-config.js.map +1 -1
  68. package/dist/index.d.ts +30 -33
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +14 -13
  71. package/dist/index.js.map +1 -1
  72. package/dist/wire/Invocation.d.ts +1 -3
  73. package/dist/wire/Invocation.d.ts.map +1 -1
  74. package/dist/wire/Invocation.js +1 -5
  75. package/dist/wire/Invocation.js.map +1 -1
  76. package/dist/wire/binding.d.ts +9 -0
  77. package/dist/wire/binding.d.ts.map +1 -1
  78. package/dist/wire/binding.js +11 -6
  79. package/dist/wire/binding.js.map +1 -1
  80. package/dist/wire/emit.d.ts +35 -3
  81. package/dist/wire/emit.d.ts.map +1 -1
  82. package/dist/wire/emit.js +26 -3
  83. package/dist/wire/emit.js.map +1 -1
  84. package/dist/wire/loggerMiddleware.d.ts.map +1 -1
  85. package/dist/wire/loggerMiddleware.js +6 -0
  86. package/dist/wire/loggerMiddleware.js.map +1 -1
  87. package/package.json +4 -4
  88. package/src/boot/AppLifecycle.ts +8 -0
  89. package/src/boot/Emissions.ts +176 -8
  90. package/src/boot/bootstrap.ts +89 -17
  91. package/src/boot/hosted.ts +3 -1
  92. package/src/boot/install.ts +35 -4
  93. package/src/boot/ports.ts +71 -27
  94. package/src/boot/remote.ts +3 -3
  95. package/src/boot/types.ts +16 -1
  96. package/src/builtin/LogLine.ts +38 -0
  97. package/src/builtin/logger.ts +109 -17
  98. package/src/config-loader.ts +2 -1
  99. package/src/contract.ts +5 -9
  100. package/src/conventions.ts +9 -2
  101. package/src/declare.ts +19 -1
  102. package/src/descriptor/frond.ts +31 -0
  103. package/src/descriptor/index.ts +1 -1
  104. package/src/dispatch/ArgumentResolver.ts +6 -2
  105. package/src/dispatch/HandlerFacade.ts +2 -2
  106. package/src/effective-operation.ts +13 -7
  107. package/src/frond-config.ts +20 -0
  108. package/src/index.ts +36 -50
  109. package/src/wire/Invocation.ts +2 -7
  110. package/src/wire/binding.ts +17 -6
  111. package/src/wire/emit.ts +58 -6
  112. package/src/wire/loggerMiddleware.ts +6 -0
@@ -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 collectorcould 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
+ }
@@ -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 './storage/criterion.js';
13
- export type { Comparison, ComparisonName } from './storage/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
  }
package/src/declare.ts CHANGED
@@ -1,7 +1,7 @@
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
7
  import { DEFAULT_CONVENTIONS } from './conventions.js';
@@ -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
  }
@@ -8,7 +8,7 @@ export { Fronds } from './Fronds.js';
8
8
  export { cardinalityOf, type OperationContract, type OperationsMap, type Param, type TypeRef } from '../wire/operation.js';
9
9
  export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
10
10
  export type { Signature } from '../wire/signature.js';
11
- export { emitKeyOf } from '../wire/emit.js';
11
+ export { emitKeyOf, awaitKeyOf } from '../wire/emit.js';
12
12
  export { storageKeyOf } from '../storage/port.js';
13
13
  export { getPresenterFields } from '../prefab/presenter.js';
14
14
  export { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
@@ -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
@@ -13,7 +13,7 @@ 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
15
  import type { BindingPlan } from '../wire/binding.js';
16
- import { canonicalInvocation, type InvocationContext } from '../wire/Invocation.js';
16
+ import { Invocation, type InvocationContext } from '../wire/Invocation.js';
17
17
  import type { HandlerEntry, PresenterEntry } from '../descriptor/frond.js';
18
18
  import { ArgumentResolver } from './ArgumentResolver.js';
19
19
  import { OutputView } from './OutputView.js';
@@ -101,7 +101,7 @@ export class HandlerFacade {
101
101
  );
102
102
  }
103
103
 
104
- const invocation = canonicalInvocation(input);
104
+ const invocation = Invocation.from(input);
105
105
  const context: OperationContext = {
106
106
  entity,
107
107
  frond: this.door.frond,
@@ -229,13 +229,19 @@ export function resolveEffectiveOperations(
229
229
 
230
230
  // A fact is validated by the entity it names. This used to be patched into the
231
231
  // facade after resolution, leaving check/explain with a different input.
232
- let input = contract.input;
233
- if (!input) {
234
- const fact = contract.binding.find((binding) => binding.source.kind === 'fact');
235
- if (fact?.source.kind === 'fact') input = schemas.get(fact.source.factName);
236
- }
237
-
238
- const output = effectiveOutput(frond, handler, name, contract);
232
+ const announced = contract.binding.find((binding) =>
233
+ binding.source.kind === 'fact' || binding.source.kind === 'pipe');
234
+ const fact = announced?.source.kind === 'fact' || announced?.source.kind === 'pipe'
235
+ ? schemas.get(announced.source.factName)
236
+ : undefined;
237
+ const input = contract.input ?? fact;
238
+
239
+ // An op that FINISHES a fact answers the fact, so its answer is the fact's shape:
240
+ // derived here, where its input already is. Left to the ordinary projection it
241
+ // came back stripped — measured, the subscriber got a value with no `id`.
242
+ const output = announced?.source.kind === 'pipe' && fact
243
+ ? { schema: fact, closed: false }
244
+ : effectiveOutput(frond, handler, name, contract);
239
245
  const className = handler.ctor.name.endsWith('Handler')
240
246
  ? handler.ctor.name.slice(0, -'Handler'.length)
241
247
  : handler.ctor.name;
@@ -11,6 +11,12 @@ export interface OperationOverride {
11
11
  kind?: 'query' | 'command';
12
12
  /** The GraphQL root field this op answers to. */
13
13
  graphql?: string;
14
+ /**
15
+ * Where this op answers over REST — the dual of `graphql:`, and the frond's to say for
16
+ * the same reason: how an operation is CALLED belongs beside it, not in whatever host
17
+ * mounts the router. The prefix stays the host's: naming is the frond's, mounting is not.
18
+ */
19
+ rest?: { method?: string; path?: string; status?: number };
14
20
  /**
15
21
  * Handler class to delegate to (overrides the default `{Entity}Handler` lookup).
16
22
  * Class name is used to resolve from DI. E.g. `ArchiveHandler` → `app.resolve('ArchiveHandler')`.
@@ -44,6 +50,20 @@ export interface FrondConfig {
44
50
  surfaces?: Record<string, string[]>;
45
51
  /** Interface → implementation bindings for DI (e.g. { Database: 'SqliteDatabase' }). */
46
52
  bindings?: Record<string, string>;
53
+ /**
54
+ * The ops that FINISH a fact, in the order they run — by class name, keyed by fact.
55
+ *
56
+ * Declared by the frond that OWNS the fact, because ordering is a decision about the
57
+ * fact itself and it has one owner. Without it two links refuse: nothing would say which
58
+ * ran first, and scan order is not an answer.
59
+ */
60
+ pipes?: Record<string, string[]>;
61
+ /**
62
+ * How far a middleware reaches, by class name. A middleware answers for its own frond
63
+ * without being named here; `'app'` is the exception, and it is stated by the frond
64
+ * that decides for the others.
65
+ */
66
+ middlewares?: Record<string, 'frond' | 'app'>;
47
67
  /** Per-operation overrides. Key = operation name (method name on a handler). */
48
68
  operations?: Record<string, OperationOverride>;
49
69
  }
package/src/index.ts CHANGED
@@ -4,87 +4,74 @@ export { orderSeeds, runSeeds, seeding } from './boot/seed.js';
4
4
  export { AppLifecycle, migrating } from './boot/AppLifecycle.js';
5
5
  export type { Extension } from './boot/AppLifecycle.js';
6
6
  export { defineFougere } from './define.js';
7
- export type { AdapterConfig, FougereConfig } from './config-loader.js';
8
- export type { FrondConfig, OperationOverride } from './frond-config.js';
7
+ export type { FougereConfig, AdapterConfig } from './config-loader.js';
8
+ export type { OperationOverride, FrondConfig } from './frond-config.js';
9
9
  export { buildGraph, clusterEntities, suggestSplit, type EntityNode, type DomainCluster } from './graph.js';
10
10
  export { Crud } from './prefab/crud.js';
11
- export type { CrudOps, CrudOpName, CrudViews, CrudConstructor } from './prefab/crud.js';
11
+ export type { CrudViews, CrudOps, CrudOpName, CrudConstructor } from './prefab/crud.js';
12
12
  export { Mirror } from './prefab/mirror.js';
13
- export type { MirrorOf, MirrorConstructor, Refreshed } from './prefab/mirror.js';
13
+ export type { Refreshed, MirrorOf, MirrorConstructor } from './prefab/mirror.js';
14
14
  export { Presenter } from './prefab/presenter.js';
15
15
  export type { PresenterViews } from './prefab/presenter.js';
16
16
  export { Collector } from './prefab/collector.js';
17
17
  export { Repository, type RepositoryOf, type RepositoryConstructor, type AggregateOf, type AggregateConstructor } from './prefab/repository.js';
18
- export { Invocation, canonicalInvocation, EMPTY_INVOCATION } from './wire/Invocation.js';
19
- export type { InvocationContext, PartialInvocation } from './wire/Invocation.js';
20
- export { Call } from './wire/call.js';
21
- export { RouteAddress } from './wire/RouteAddress.js';
22
- export type { RouteAddressInput } from './wire/RouteAddress.js';
18
+ export { Invocation } from './contract.js';
19
+ export type { InvocationContext } from './contract.js';
20
+ export { Call } from './contract.js';
21
+ export { RouteAddress } from './contract.js';
23
22
  export { DispatchLifecycle } from './dispatch/DispatchLifecycle.js';
24
23
  export { Dispatcher } from './dispatch/Dispatcher.js';
25
24
  // The class and not only its type: an observer RECEIVES these, so a package that folds
26
25
  // them — or a test of one — has to be able to make one through the door.
27
26
  export { DispatchEvent } from './dispatch/DispatchEvent.js';
28
- export type { CallPage, CallRecord } from './wire/CallLog.js';
29
- export { driftOf, agrees, explain, type CardDrift } from './wire/drift.js';
30
- export type { DispatchObserver } from './dispatch/DispatchEvent.js';
31
- export type { DispatchPort } from './dispatch/DispatchPort.js';
32
- export {
33
- inferOperationKind,
34
- resolveIsReadOp,
35
- resolveOperationKind,
36
- } from './wire/operation.js';
37
- export { resolveContracts } from './effective-operation.js';
27
+ export type { CallPage, CallRecord } from './contract.js';
28
+ export { driftOf, agrees, explain, type CardDrift } from './contract.js';
38
29
  export type {
39
30
  OperationContract,
40
- OperationKind,
41
- OperationKindInference,
42
31
  OperationsMap,
43
32
  Param,
44
33
  TypeRef,
45
34
  } from './wire/operation.js';
46
- export {
47
- EFFECTIVE_OPERATION_SEMANTICS,
48
- EffectiveOperationModel,
49
- resolveEffectiveOperations,
50
- } from './effective-operation.js';
51
- export type {
52
- EffectiveCollector,
53
- EffectiveOperation,
54
- EffectiveOperationOptions,
55
- EffectiveOperationsMap,
56
- EffectiveParameter,
57
- } from './effective-operation.js';
35
+ export { resolveEffectiveOperations } from './effective-operation.js';
36
+ export type { EffectiveOperation } from './effective-operation.js';
58
37
  export type { ModuleLoader } from './loader.js';
59
- export { FougereError, ErrorCode, validationErrorsOf, type FougereErrorOptions } from './wire/errors.js';
60
- export type { OperationContext, AppNext, AppMiddleware } from './wire/middleware.js';
61
- export { assertIdentityCard, RPC_ENTITY, MAX_BODY_BYTES } from './wire/call.js';
38
+ export { FougereError, ErrorCode, validationErrorsOf } from './contract.js';
39
+ export type { AppMiddleware, OperationContext, AppNext } from './wire/middleware.js';
40
+ export { assertIdentityCard, RPC_ENTITY, MAX_BODY_BYTES } from './contract.js';
62
41
  export { createLocalRunner, createAppRunner } from './boot/runner.js';
63
42
 
64
43
  // A frond an app STATES, for a boot that will not scan. No disk, no `typescript` — which
65
44
  // is why it sits here and not in `/node`, beside the scanner that reads a project.
66
45
  export { frond } from './declare.js';
67
- export type { FrondDeclaration, Declared, DeclaredSubject } from './declare.js';
46
+ export type { Declared, FrondDeclaration, DeclaredSubject } from './declare.js';
68
47
  // What this app would answer to `rpc.discover`. Served by the boot, and reached until now
69
48
  // only by tests through a deep path — a consumer comparing its own card to a producer's
70
49
  // had to dispatch a call to ask a question about itself.
71
50
  export { identityCardOf } from './boot/card.js';
51
+ export type { Facade } from './wire/call.js';
72
52
  export type {
73
- FrondCall, Transport, IdentityCard, CardOp, Facade, RpcAnswer,
74
- TopologyReport, FrondPlacement, Edge,
75
- } from './wire/call.js';
76
- export { type Emit, type Fact } from './wire/emit.js';
53
+ FrondCall,
54
+ Transport,
55
+ IdentityCard,
56
+ CardOp,
57
+ TopologyReport,
58
+ FrondPlacement,
59
+ Edge,
60
+ } from './contract.js';
61
+ export { type Emit, type Fact, type Pipe } from './wire/emit.js';
77
62
  export { callValueOf } from './contract.js';
78
- export type { CallValue } from './contract.js';
79
- export { toHttpError, toPublicError } from './wire/http-error.js';
63
+ export { toHttpError } from './wire/http-error.js';
64
+ export { toPublicError } from './contract.js';
80
65
  export { loggerMiddleware } from './wire/loggerMiddleware.js';
81
- export { Logger, setLogLevel, logLevel, envLevel, onLog } from './builtin/logger.js';
82
- export type { LogLevel, LogRecord, LogSink } from './builtin/logger.js';
66
+ export { Carry, Logger, setLogLevel, logLevel, envLevel, onLog, formatted } from './builtin/logger.js';
67
+ export type { LogRecord, Rendered, LogSink } from './builtin/logger.js';
68
+ // The fact a boot announces. Its destinations are `@fougere/log`'s; the line is core's.
69
+ export { default as LogLine, CARRIES_LINE, LOG_LINE } from './builtin/LogLine.js';
83
70
  // What a re-read config changes in a running process — and what it cannot.
84
- export { applyConfig, type ConfigApplication } from './boot/apply.js';
71
+ export { applyConfig } from './boot/apply.js';
85
72
  export { Config } from './builtin/config.js';
86
73
  export type { Storage, StorageFactory, ListOptions, ListResult, Together } from './storage/port.js';
87
- export { togetherKeyOf, membersOfTogetherKey } from './storage/port.js';
74
+ export { togetherKeyOf } from './storage/port.js';
88
75
  export type { Constraint, Source, SourceConfig, SourceView } from './source.js';
89
76
  export { Sources } from './source.js';
90
77
  export { storageOver } from './storage/store.js';
@@ -102,17 +89,16 @@ export type {
102
89
  EntityEntry,
103
90
  HandlerEntry,
104
91
  SeedEntry,
105
- SeedFactory,
106
92
  PresenterEntry,
107
93
  PresenterFieldMeta,
108
94
  CollectorEntry,
109
95
  } from './descriptor/frond.js';
110
96
  export type { AuthConfig, AuthContext, AuthRuntime } from './boot/auth.js';
111
97
  export { Fronds } from './descriptor/Fronds.js';
112
- export { verify, assertSplittable, type Violation } from './verify.js';
98
+ export { verify, type Violation } from './verify.js';
113
99
  // Same question as verify(), answered from the source text instead of the model.
114
100
  // Who is calling, established rather than accepted — the proof beside `state`'s claim.
115
101
  // `generateKeyPair` and `issueGrant` are NOT here: they make keys at a deployment and
116
102
  // need `node:crypto` for a gesture WebCrypto has no equal of. They sit on `/node`.
117
103
  export { signEnvelope, verifyEnvelope, identityFromEnv } from './identity.js';
118
- export type { FrondIdentity, VerifiedCall, CallIdentity, SignedCall } from './identity.js';
104
+ export type { FrondIdentity, SignedCall } from './identity.js';
@@ -60,6 +60,8 @@ export class Invocation implements InvocationContext {
60
60
  return context instanceof Invocation ? context : new Invocation(context ?? {});
61
61
  }
62
62
 
63
+ static readonly empty = Invocation.from();
64
+
63
65
  /** Replaces the input — how the façade hands on the value it parsed. */
64
66
  withInput(input: unknown): Invocation {
65
67
  return new Invocation({ ...this, input });
@@ -70,10 +72,3 @@ export class Invocation implements InvocationContext {
70
72
  return new Invocation({ ...this, state });
71
73
  }
72
74
  }
73
-
74
- export const EMPTY_INVOCATION = Invocation.from();
75
-
76
- /** Names the same gesture as `Invocation.from` for callers that read better this way. */
77
- export function canonicalInvocation(context?: PartialInvocation): Invocation {
78
- return Invocation.from(context);
79
- }