@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.
- package/dist/boot/AppLifecycle.d.ts +8 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Emissions.d.ts +26 -0
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +143 -8
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +77 -18
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +1 -0
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/install.d.ts +2 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +33 -4
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ports.d.ts +12 -3
- package/dist/boot/ports.d.ts.map +1 -1
- package/dist/boot/ports.js +51 -21
- package/dist/boot/ports.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +3 -3
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/types.d.ts +16 -1
- package/dist/boot/types.d.ts.map +1 -1
- package/dist/builtin/LogLine.d.ts +38 -0
- package/dist/builtin/LogLine.d.ts.map +1 -0
- package/dist/builtin/LogLine.js +37 -0
- package/dist/builtin/LogLine.js.map +1 -0
- package/dist/builtin/logger.d.ts +57 -0
- package/dist/builtin/logger.d.ts.map +1 -1
- package/dist/builtin/logger.js +72 -17
- package/dist/builtin/logger.js.map +1 -1
- package/dist/config-loader.d.ts +2 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js.map +1 -1
- package/dist/contract.d.ts +3 -7
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +3 -4
- package/dist/contract.js.map +1 -1
- package/dist/conventions.d.ts +6 -0
- package/dist/conventions.d.ts.map +1 -1
- package/dist/conventions.js +3 -2
- package/dist/conventions.js.map +1 -1
- package/dist/declare.d.ts +11 -1
- package/dist/declare.d.ts.map +1 -1
- package/dist/declare.js +9 -0
- package/dist/declare.js.map +1 -1
- package/dist/descriptor/frond.d.ts +34 -0
- package/dist/descriptor/frond.d.ts.map +1 -1
- package/dist/descriptor/index.d.ts +1 -1
- package/dist/descriptor/index.d.ts.map +1 -1
- package/dist/descriptor/index.js +1 -1
- package/dist/descriptor/index.js.map +1 -1
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/ArgumentResolver.js +6 -2
- package/dist/dispatch/ArgumentResolver.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +2 -2
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +11 -7
- package/dist/effective-operation.js.map +1 -1
- package/dist/frond-config.d.ts +24 -0
- package/dist/frond-config.d.ts.map +1 -1
- package/dist/frond-config.js.map +1 -1
- package/dist/index.d.ts +30 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -13
- package/dist/index.js.map +1 -1
- package/dist/wire/Invocation.d.ts +1 -3
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +1 -5
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/binding.d.ts +9 -0
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js +11 -6
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/emit.d.ts +35 -3
- package/dist/wire/emit.d.ts.map +1 -1
- package/dist/wire/emit.js +26 -3
- package/dist/wire/emit.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js +6 -0
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +4 -4
- package/src/boot/AppLifecycle.ts +8 -0
- package/src/boot/Emissions.ts +176 -8
- package/src/boot/bootstrap.ts +89 -17
- package/src/boot/hosted.ts +3 -1
- package/src/boot/install.ts +35 -4
- package/src/boot/ports.ts +71 -27
- package/src/boot/remote.ts +3 -3
- package/src/boot/types.ts +16 -1
- package/src/builtin/LogLine.ts +38 -0
- package/src/builtin/logger.ts +109 -17
- package/src/config-loader.ts +2 -1
- package/src/contract.ts +5 -9
- package/src/conventions.ts +9 -2
- package/src/declare.ts +19 -1
- package/src/descriptor/frond.ts +31 -0
- package/src/descriptor/index.ts +1 -1
- package/src/dispatch/ArgumentResolver.ts +6 -2
- package/src/dispatch/HandlerFacade.ts +2 -2
- package/src/effective-operation.ts +13 -7
- package/src/frond-config.ts +20 -0
- package/src/index.ts +36 -50
- package/src/wire/Invocation.ts +2 -7
- package/src/wire/binding.ts +17 -6
- package/src/wire/emit.ts +58 -6
- package/src/wire/loggerMiddleware.ts +6 -0
package/src/boot/bootstrap.ts
CHANGED
|
@@ -7,7 +7,13 @@ import { installFrond, type Assembly } from './install.js';
|
|
|
7
7
|
import type { AuthRuntime } from './auth.js';
|
|
8
8
|
import type { CreateAppOptions, App } from './types.js';
|
|
9
9
|
import type { AppMiddleware } from '../wire/middleware.js';
|
|
10
|
-
import { Logger } from '../builtin/logger.js';
|
|
10
|
+
import { Carry, Logger } from '../builtin/logger.js';
|
|
11
|
+
import type { LogRecord } from '../builtin/logger.js';
|
|
12
|
+
import LogLine, { CARRIES_LINE } from '../builtin/LogLine.js';
|
|
13
|
+
import { emitKeyOf, type Emit } from '../wire/emit.js';
|
|
14
|
+
|
|
15
|
+
/** The fact the boot announces, spelled once. */
|
|
16
|
+
const LOG_LINE = lowerFirst(LogLine.name);
|
|
11
17
|
import { Config } from '../builtin/config.js';
|
|
12
18
|
import { createRemoteRouter, createRemoteFacade } from './remote.js';
|
|
13
19
|
import { registerFrames } from './together.js';
|
|
@@ -26,7 +32,8 @@ import { InFlight } from '../dispatch/InFlight.js';
|
|
|
26
32
|
// The keys, each read from where its concept is declared — never respelled here.
|
|
27
33
|
import { facadeKeyOf, contractsKeyOf, type RpcAnswer } from '../wire/call.js';
|
|
28
34
|
import { identityCardOf } from './card.js';
|
|
29
|
-
import { AppLifecycle } from './AppLifecycle.js';
|
|
35
|
+
import { AppLifecycle, migrating } from './AppLifecycle.js';
|
|
36
|
+
import { seeding } from './seed.js';
|
|
30
37
|
import { inheritsCrud, subjectOf } from '../prefab/crud.js';
|
|
31
38
|
import { repositoryKeyOf } from '../prefab/repository.js';
|
|
32
39
|
import { storageKeyOf } from '../storage/port.js';
|
|
@@ -83,9 +90,20 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
83
90
|
|
|
84
91
|
// Held out here, and not where the ascent reads it, because releasing needs it and
|
|
85
92
|
// releasing has to work from the first line the boot takes something.
|
|
86
|
-
|
|
93
|
+
// The conventional ascent, ordered here: tables, then rows, then whatever the host took
|
|
94
|
+
// on. Four hosts assembled these two members themselves — the order is not theirs to
|
|
95
|
+
// choose, and a host that forgot lost its migration in silence.
|
|
96
|
+
const appLifecycle = new AppLifecycle().add(
|
|
97
|
+
migrating(options.migrate),
|
|
98
|
+
seeding(),
|
|
99
|
+
...(options.extensions ?? []),
|
|
100
|
+
);
|
|
87
101
|
/** The app once it exists — a refusal before that releases the two levels that do. */
|
|
88
102
|
let built: App | undefined;
|
|
103
|
+
/** Where THIS boot's lines wait — never a process-wide slot, see `Carry`. */
|
|
104
|
+
const carry = new Carry();
|
|
105
|
+
/** Given back by `carry.to`, so a released app stops writing into a dead container. */
|
|
106
|
+
let stopAnnouncing: (() => void) | undefined;
|
|
89
107
|
|
|
90
108
|
/**
|
|
91
109
|
* Everything this app holds, let go in reverse of how it was taken: what an extension took on
|
|
@@ -97,6 +115,9 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
97
115
|
// there and broken here, a refusing extension took the container and the connection
|
|
98
116
|
// down with it, which is the leak this gesture exists to prevent.
|
|
99
117
|
const refused: unknown[] = [];
|
|
118
|
+
// Whatever is still held will never reach a destination — the console had it.
|
|
119
|
+
stopAnnouncing?.();
|
|
120
|
+
carry.forget();
|
|
100
121
|
const levels = [
|
|
101
122
|
...(built ? [() => appLifecycle.down(built!)] : []),
|
|
102
123
|
() => container.dispose(),
|
|
@@ -119,14 +140,14 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
119
140
|
|
|
120
141
|
try {
|
|
121
142
|
// Boot chatter is debug by default; a host (e.g. the CLI) can quiet it.
|
|
122
|
-
const log = new Logger('boot:app');
|
|
143
|
+
const log = new Logger('boot:app', { carry });
|
|
123
144
|
|
|
124
145
|
// Builtins — registered under class name (PascalCase) for type-based DI.
|
|
125
146
|
// No level here and none anywhere: a logger consults `setLogLevel`'s value at each
|
|
126
147
|
// emission, so this instance survives a level change and so does every handler that
|
|
127
148
|
// was handed it. A frond declaring `class X extends Logger` takes this key over,
|
|
128
149
|
// like any other port.
|
|
129
|
-
container.registerValue('Logger', new Logger('app'));
|
|
150
|
+
container.registerValue('Logger', new Logger('app', { carry }));
|
|
130
151
|
container.register('Config', Config, { lifetime: 'singleton' });
|
|
131
152
|
log.debug('builtins registered (Logger, Config)');
|
|
132
153
|
|
|
@@ -134,12 +155,20 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
134
155
|
// it may read a disk; consuming it never does, which is the whole reason this file names
|
|
135
156
|
// no builtin and a Worker can run what it builds.
|
|
136
157
|
const scanStart = performance.now();
|
|
137
|
-
|
|
158
|
+
// An extension's fronds sit beside the app's own: it is installed like any other, and
|
|
159
|
+
// its handlers resolve at call time — by which point the extension's `up` has put what
|
|
160
|
+
// they ask for in the container.
|
|
161
|
+
const brought = (options.extensions ?? [])
|
|
162
|
+
.flatMap((extension) => extension?.fronds ?? [])
|
|
163
|
+
.map((frond) => ({ ...frond, brought: true as const }));
|
|
164
|
+
const { fronds, diagnostics } = await hostedBy(
|
|
165
|
+
brought.length > 0 ? { ...options, fronds: [...(options.fronds ?? []), ...brought] } : options,
|
|
166
|
+
);
|
|
138
167
|
// An app that states nothing AND scans nothing is a mistake — unless something else it
|
|
139
168
|
// declares brings its own entities, which an auth provider does. Refused here and not in
|
|
140
169
|
// `hostedBy`, which is handed the frond sources and cannot see the rest of the app. The
|
|
141
170
|
// condition is the KEYS, not the count: a scan that found nothing is an ordinary answer.
|
|
142
|
-
if (!options.fronds && !options.scan && !options.auth) {
|
|
171
|
+
if (!options.fronds && !options.scan && !options.auth && brought.length === 0) {
|
|
143
172
|
throw new Error(
|
|
144
173
|
'createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
|
|
145
174
|
+ 'Neither was given, and nothing else declares entities of its own.',
|
|
@@ -233,15 +262,41 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
233
262
|
return [...globalMiddlewares, ...scoped];
|
|
234
263
|
}
|
|
235
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The one place a middleware is taken on. `App.use` is its late form, and a frond's
|
|
267
|
+
* `middlewares/` its early one — the app does not exist yet while fronds install.
|
|
268
|
+
*/
|
|
269
|
+
function use(middleware: AppMiddleware, entity?: string): void {
|
|
270
|
+
if (entity === undefined) {
|
|
271
|
+
globalMiddlewares.push(middleware);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const scoped = scopedMiddlewares.get(entity) ?? [];
|
|
275
|
+
scoped.push(middleware);
|
|
276
|
+
scopedMiddlewares.set(entity, scoped);
|
|
277
|
+
}
|
|
278
|
+
|
|
236
279
|
assertOneOwnerPerKey(fronds, options.remotes);
|
|
237
280
|
|
|
238
281
|
// Every entity of every frond, by name — so a fact can be validated where it LANDS, and
|
|
239
282
|
// so a `reads:` clause can name a neighbour's.
|
|
240
283
|
const entityByName = fronds.schemas();
|
|
284
|
+
// The line is core's, so its SHAPE is too: a destination that declares only a handler
|
|
285
|
+
// would otherwise be handed a line with no `at` — the announcement stamps `created()`
|
|
286
|
+
// off the shape, and the strict judge refuses what it did not stamp. Measured on
|
|
287
|
+
// `demos/observability`, where the ring held 11 calls and 0 lines.
|
|
288
|
+
if (!entityByName.has(LOG_LINE)) entityByName.set(LOG_LINE, LogLine);
|
|
241
289
|
// Which frond holds an entity — what turns "a member is remote" into a refusal that
|
|
242
290
|
// names the frond rather than the entity, since `remotes:` is declared per frond.
|
|
243
291
|
const frondOf = new Map(fronds.flatMap((f) => f.entities.map((e) => [e.name, f.name] as const)));
|
|
244
|
-
|
|
292
|
+
// Its own writer, which does NOT announce: this is what carries a fact, and a line
|
|
293
|
+
// about carrying one would come back here. See `LoggerOptions.carries`.
|
|
294
|
+
const emissions = new Emissions(
|
|
295
|
+
fronds, entityByName, container,
|
|
296
|
+
// No carry: this is what CARRIES a fact, and a line about carrying one comes back.
|
|
297
|
+
new Logger('boot:app'),
|
|
298
|
+
options.onEmit,
|
|
299
|
+
);
|
|
245
300
|
/** Canonical operation tables, indexed by the same audience key as their facades. */
|
|
246
301
|
const effectiveByKey = new Map<string, EffectiveOperationsMap>();
|
|
247
302
|
|
|
@@ -258,7 +313,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
258
313
|
// table, one emission list — so what a frond serves is there for the next one to find.
|
|
259
314
|
const assembly: Assembly = {
|
|
260
315
|
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
261
|
-
boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares,
|
|
316
|
+
boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
|
|
262
317
|
log, options,
|
|
263
318
|
};
|
|
264
319
|
for (const frond of fronds) await installFrond(frond, assembly);
|
|
@@ -277,6 +332,28 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
277
332
|
// Once every door exists: what is announced here and what is listened to are both known.
|
|
278
333
|
emissions.register();
|
|
279
334
|
|
|
335
|
+
// The boot's own lines, and every line after them. Held until here because a boot
|
|
336
|
+
// writes most of what a process logs and writes it before any door exists — so the
|
|
337
|
+
// lines that say what this app is made of are the ones a destination would miss.
|
|
338
|
+
// `LogLine` is core's for this reason: naming it costs no optional package.
|
|
339
|
+
// Which doors carry a line, read from who SUBSCRIBED — so a third party's destination
|
|
340
|
+
// is left alone by the two middlewares that observe every operation.
|
|
341
|
+
for (const door of emissions.doorsFor(LOG_LINE)) {
|
|
342
|
+
CARRIES_LINE.add(door.replace(/Handler$/, '').replace(/^./, (c) => c.toLowerCase()));
|
|
343
|
+
CARRIES_LINE.add(door);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (emissions.listensTo().includes(LOG_LINE)) {
|
|
347
|
+
const emit = container.resolve<Emit<LogLine>>(emitKeyOf(LogLine.name));
|
|
348
|
+
// `at` is the record's own epoch, and the entity says `created()` — so the line
|
|
349
|
+
// keeps WHEN IT WAS WRITTEN rather than when it was handed over, which for a held
|
|
350
|
+
// boot line is a different moment.
|
|
351
|
+
stopAnnouncing = carry.to(({ at, ...line }: LogRecord) => void emit({ ...line, at: new Date(at) }));
|
|
352
|
+
} else {
|
|
353
|
+
// No destination in this app: the console had them, and holding more would grow.
|
|
354
|
+
carry.forget();
|
|
355
|
+
}
|
|
356
|
+
|
|
280
357
|
/** The last resort, held by the container so every resolution path shares it. */
|
|
281
358
|
container.setFallback?.((name) => {
|
|
282
359
|
if (!remoteRouter) return undefined;
|
|
@@ -485,14 +562,9 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
485
562
|
return dispatchLifecycle.add(observer);
|
|
486
563
|
},
|
|
487
564
|
use(...args: [AppMiddleware] | [string, AppMiddleware]): void {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
list.push(mw);
|
|
492
|
-
scopedMiddlewares.set(entity, list);
|
|
493
|
-
} else {
|
|
494
|
-
globalMiddlewares.push(args[0] as AppMiddleware);
|
|
495
|
-
}
|
|
565
|
+
return typeof args[0] === 'string'
|
|
566
|
+
? use(args[1] as AppMiddleware, args[0])
|
|
567
|
+
: use(args[0] as AppMiddleware);
|
|
496
568
|
},
|
|
497
569
|
auth: authRuntime,
|
|
498
570
|
};
|
package/src/boot/hosted.ts
CHANGED
|
@@ -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
|
|
package/src/boot/install.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface Assembly {
|
|
|
52
52
|
contractsOf: (operations: EffectiveOperationsMap) => OperationsMap;
|
|
53
53
|
/** Read at call time and never at boot, so a late registration still applies. */
|
|
54
54
|
getMiddlewares: (entity: string) => AppMiddleware[];
|
|
55
|
+
/** Take a middleware on — every entity when no entity is named. */
|
|
56
|
+
use: (middleware: AppMiddleware, entity?: string) => void;
|
|
55
57
|
log: Logger;
|
|
56
58
|
options: CreateAppOptions;
|
|
57
59
|
}
|
|
@@ -59,7 +61,7 @@ export interface Assembly {
|
|
|
59
61
|
export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
|
|
60
62
|
const {
|
|
61
63
|
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
62
|
-
boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares,
|
|
64
|
+
boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
|
|
63
65
|
log, options,
|
|
64
66
|
} = assembly;
|
|
65
67
|
|
|
@@ -126,10 +128,22 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
126
128
|
// reaches the realization instead of the base class it is declared against.
|
|
127
129
|
// Registered AFTER the loop above so a port key always wins over the base's
|
|
128
130
|
// own registration — same precedence as a declared repository over its default.
|
|
129
|
-
for (const [port,
|
|
130
|
-
|
|
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
|
+
});
|
|
131
145
|
boundPorts.add(port);
|
|
132
|
-
frondLog.debug(`port ${port} → ${
|
|
146
|
+
frondLog.debug(`port ${port} → ${chain.map((one) => one.ctor.name).join(' → ')}`);
|
|
133
147
|
}
|
|
134
148
|
if (frond.providers.length > 0) {
|
|
135
149
|
frondLog.debug(`${frond.providers.length} provider(s): ${frond.providers.map(nameOf).join(', ')}`);
|
|
@@ -226,6 +240,23 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
226
240
|
frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
|
|
227
241
|
}
|
|
228
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
|
+
|
|
229
260
|
// Build handler facades → registered in ROOT container (public contract)
|
|
230
261
|
const defaultHandlers = frond.handlers.filter((h) => !h.surface);
|
|
231
262
|
const surfaceHandlers = frond.handlers.filter((h) => h.surface);
|
package/src/boot/ports.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { nameOf, type ProviderEntry } from '../descriptor/frond.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
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,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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}: '${
|
|
33
|
-
+ `What does: ${
|
|
65
|
+
`[ports] ${port}: '${name}' does not extend it. `
|
|
66
|
+
+ `What does: ${all.map(nameOf).join(', ')}.`,
|
|
34
67
|
);
|
|
35
68
|
}
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/boot/remote.ts
CHANGED
|
@@ -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 {
|
|
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' },
|
|
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 =
|
|
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 = {
|
package/src/boot/types.ts
CHANGED
|
@@ -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
|
-
|
|
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>();
|