@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.
- 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 +144 -9
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/auth.d.ts +1 -1
- package/dist/boot/auth.d.ts.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +401 -598
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.js +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/hosted.d.ts +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 +42 -0
- package/dist/boot/install.d.ts.map +1 -0
- package/dist/boot/install.js +327 -0
- package/dist/boot/install.js.map +1 -0
- package/dist/boot/ownership.d.ts +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +14 -1
- package/dist/boot/ownership.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/seed.d.ts +7 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +7 -2
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +1 -1
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/together.d.ts +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/boot/types.d.ts +18 -3
- 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 +3 -2
- 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/{scan/conventions.d.ts → conventions.d.ts} +6 -0
- package/dist/conventions.d.ts.map +1 -0
- package/dist/{scan/conventions.js → conventions.js} +3 -2
- package/dist/conventions.js.map +1 -0
- package/dist/crypto/encoding.d.ts +0 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js +0 -7
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/declare.d.ts +11 -1
- package/dist/declare.d.ts.map +1 -1
- package/dist/declare.js +10 -1
- 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 +15 -0
- package/dist/descriptor/index.d.ts.map +1 -0
- package/dist/descriptor/index.js +14 -0
- package/dist/descriptor/index.js.map +1 -0
- package/dist/descriptor/surface.d.ts +8 -0
- package/dist/descriptor/surface.d.ts.map +1 -0
- package/dist/descriptor/surface.js +16 -0
- package/dist/descriptor/surface.js.map +1 -0
- 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 +2 -0
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +9 -4
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +9 -23
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/dispatch/presenterArguments.d.ts +7 -1
- package/dist/dispatch/presenterArguments.d.ts.map +1 -1
- package/dist/dispatch/presenterArguments.js +15 -4
- package/dist/dispatch/presenterArguments.js.map +1 -1
- package/dist/dispatch/validateInput.d.ts.map +1 -1
- package/dist/dispatch/validateInput.js +2 -2
- package/dist/dispatch/validateInput.js.map +1 -1
- package/dist/effective-operation.d.ts +1 -1
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +13 -19
- 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 +35 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -14
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -12
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -12
- package/dist/node.js.map +1 -1
- package/dist/prefab/crud.d.ts +1 -1
- package/dist/prefab/crud.d.ts.map +1 -1
- package/dist/prefab/mirror.d.ts +10 -11
- package/dist/prefab/mirror.d.ts.map +1 -1
- package/dist/prefab/mirror.js +1 -27
- package/dist/prefab/mirror.js.map +1 -1
- package/dist/prefab/repository.d.ts +1 -1
- package/dist/prefab/repository.d.ts.map +1 -1
- package/dist/{scan/result.d.ts → scan.d.ts} +2 -2
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +2 -0
- package/dist/scan.js.map +1 -0
- package/dist/source.d.ts +1 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/storage/criterion.d.ts.map +1 -0
- package/dist/{criterion.js → storage/criterion.js} +2 -2
- package/dist/storage/criterion.js.map +1 -0
- package/dist/{storage.d.ts → storage/port.d.ts} +6 -3
- package/dist/storage/port.d.ts.map +1 -0
- package/dist/{storage.js → storage/port.js} +13 -6
- package/dist/storage/port.js.map +1 -0
- package/dist/{store.d.ts → storage/store.d.ts} +1 -1
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/{store.js → storage/store.js} +33 -17
- package/dist/storage/store.js.map +1 -0
- package/dist/verify.js +1 -1
- package/dist/verify.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 +45 -0
- package/dist/wire/emit.d.ts.map +1 -0
- package/dist/wire/emit.js +44 -0
- package/dist/wire/emit.js.map +1 -0
- package/dist/wire/errors.js +2 -2
- package/dist/wire/errors.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 +10 -4
- package/src/boot/AppLifecycle.ts +8 -0
- package/src/boot/Emissions.ts +177 -9
- package/src/boot/auth.ts +1 -1
- package/src/boot/bootstrap.ts +456 -672
- package/src/boot/card.ts +1 -1
- package/src/boot/hosted.ts +4 -2
- package/src/boot/install.ts +421 -0
- package/src/boot/ownership.ts +17 -2
- package/src/boot/ports.ts +71 -27
- package/src/boot/remote.ts +3 -3
- package/src/boot/seed.ts +18 -3
- package/src/boot/statement-drift.ts +1 -1
- package/src/boot/together.ts +1 -1
- package/src/boot/types.ts +18 -3
- package/src/builtin/LogLine.ts +38 -0
- package/src/builtin/logger.ts +109 -17
- package/src/config-loader.ts +3 -2
- package/src/contract.ts +5 -9
- package/src/{scan/conventions.ts → conventions.ts} +9 -2
- package/src/crypto/encoding.ts +0 -7
- package/src/declare.ts +20 -2
- package/src/descriptor/frond.ts +31 -0
- package/src/descriptor/index.ts +15 -0
- package/src/descriptor/surface.ts +23 -0
- package/src/dispatch/ArgumentResolver.ts +6 -2
- package/src/dispatch/HandlerFacade.ts +11 -4
- package/src/dispatch/StorageGuard.ts +8 -21
- package/src/dispatch/presenterArguments.ts +21 -9
- package/src/dispatch/validateInput.ts +2 -2
- package/src/effective-operation.ts +16 -21
- package/src/frond-config.ts +20 -0
- package/src/index.ts +44 -57
- package/src/node.ts +2 -16
- package/src/prefab/crud.ts +1 -1
- package/src/prefab/mirror.ts +12 -38
- package/src/prefab/repository.ts +1 -1
- package/src/{scan/result.ts → scan.ts} +1 -1
- package/src/source.ts +1 -1
- package/src/{criterion.ts → storage/criterion.ts} +2 -2
- package/src/{storage.ts → storage/port.ts} +20 -5
- package/src/{store.ts → storage/store.ts} +35 -16
- package/src/verify.ts +1 -1
- package/src/wire/Invocation.ts +2 -7
- package/src/wire/binding.ts +17 -6
- package/src/wire/call.ts +1 -1
- package/src/wire/emit.ts +84 -0
- package/src/wire/errors.ts +2 -2
- package/src/wire/loggerMiddleware.ts +6 -0
- package/dist/boot/boot.d.ts +0 -45
- package/dist/boot/boot.d.ts.map +0 -1
- package/dist/boot/boot.js +0 -63
- package/dist/boot/boot.js.map +0 -1
- package/dist/criterion.d.ts.map +0 -1
- package/dist/criterion.js.map +0 -1
- package/dist/declarations.d.ts +0 -34
- package/dist/declarations.d.ts.map +0 -1
- package/dist/declarations.js +0 -75
- package/dist/declarations.js.map +0 -1
- package/dist/emit.d.ts +0 -13
- package/dist/emit.d.ts.map +0 -1
- package/dist/emit.js +0 -21
- package/dist/emit.js.map +0 -1
- package/dist/imports.d.ts +0 -16
- package/dist/imports.d.ts.map +0 -1
- package/dist/imports.js +0 -78
- package/dist/imports.js.map +0 -1
- package/dist/placement.d.ts +0 -25
- package/dist/placement.d.ts.map +0 -1
- package/dist/placement.js +0 -69
- package/dist/placement.js.map +0 -1
- package/dist/scan/adapters.d.ts +0 -3
- package/dist/scan/adapters.d.ts.map +0 -1
- package/dist/scan/adapters.js +0 -25
- package/dist/scan/adapters.js.map +0 -1
- package/dist/scan/bundling.d.ts +0 -3
- package/dist/scan/bundling.d.ts.map +0 -1
- package/dist/scan/bundling.js +0 -15
- package/dist/scan/bundling.js.map +0 -1
- package/dist/scan/contract.d.ts +0 -27
- package/dist/scan/contract.d.ts.map +0 -1
- package/dist/scan/contract.js +0 -46
- package/dist/scan/contract.js.map +0 -1
- package/dist/scan/conventions.d.ts.map +0 -1
- package/dist/scan/conventions.js.map +0 -1
- package/dist/scan/emit.d.ts +0 -10
- package/dist/scan/emit.d.ts.map +0 -1
- package/dist/scan/emit.js +0 -144
- package/dist/scan/emit.js.map +0 -1
- package/dist/scan/handler-parser.d.ts +0 -26
- package/dist/scan/handler-parser.d.ts.map +0 -1
- package/dist/scan/handler-parser.js +0 -607
- package/dist/scan/handler-parser.js.map +0 -1
- package/dist/scan/result.d.ts.map +0 -1
- package/dist/scan/result.js +0 -2
- package/dist/scan/result.js.map +0 -1
- package/dist/scan/scanner.d.ts +0 -12
- package/dist/scan/scanner.d.ts.map +0 -1
- package/dist/scan/scanner.js +0 -573
- package/dist/scan/scanner.js.map +0 -1
- package/dist/scan/statement.d.ts +0 -4
- package/dist/scan/statement.d.ts.map +0 -1
- package/dist/scan/statement.js +0 -135
- package/dist/scan/statement.js.map +0 -1
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/src/boot/boot.ts +0 -110
- package/src/declarations.ts +0 -110
- package/src/emit.ts +0 -32
- package/src/imports.ts +0 -94
- package/src/placement.ts +0 -86
- package/src/scan/adapters.ts +0 -32
- package/src/scan/bundling.ts +0 -14
- package/src/scan/contract.ts +0 -80
- package/src/scan/emit.ts +0 -160
- package/src/scan/handler-parser.ts +0 -695
- package/src/scan/scanner.ts +0 -698
- package/src/scan/statement.ts +0 -159
- /package/dist/{criterion.d.ts → storage/criterion.d.ts} +0 -0
package/src/boot/bootstrap.ts
CHANGED
|
@@ -3,10 +3,17 @@ import { lowerFirst, type Fields, type SchemaView } from '@fougere/schema';
|
|
|
3
3
|
import { nameOf } from '../descriptor/frond.js';
|
|
4
4
|
import type { EntityEntry, HandlerEntry, PresenterEntry } from '../descriptor/frond.js';
|
|
5
5
|
import { hostedBy } from './hosted.js';
|
|
6
|
+
import { installFrond, type Assembly } from './install.js';
|
|
6
7
|
import type { AuthRuntime } from './auth.js';
|
|
7
8
|
import type { CreateAppOptions, App } from './types.js';
|
|
8
9
|
import type { AppMiddleware } from '../wire/middleware.js';
|
|
9
|
-
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);
|
|
10
17
|
import { Config } from '../builtin/config.js';
|
|
11
18
|
import { createRemoteRouter, createRemoteFacade } from './remote.js';
|
|
12
19
|
import { registerFrames } from './together.js';
|
|
@@ -25,10 +32,11 @@ import { InFlight } from '../dispatch/InFlight.js';
|
|
|
25
32
|
// The keys, each read from where its concept is declared — never respelled here.
|
|
26
33
|
import { facadeKeyOf, contractsKeyOf, type RpcAnswer } from '../wire/call.js';
|
|
27
34
|
import { identityCardOf } from './card.js';
|
|
28
|
-
import { AppLifecycle } from './AppLifecycle.js';
|
|
35
|
+
import { AppLifecycle, migrating } from './AppLifecycle.js';
|
|
36
|
+
import { seeding } from './seed.js';
|
|
29
37
|
import { inheritsCrud, subjectOf } from '../prefab/crud.js';
|
|
30
38
|
import { repositoryKeyOf } from '../prefab/repository.js';
|
|
31
|
-
import { storageKeyOf } from '../storage.js';
|
|
39
|
+
import { storageKeyOf } from '../storage/port.js';
|
|
32
40
|
import { declares } from '../source.js';
|
|
33
41
|
import { presenterKeyOf } from '../prefab/presenter.js';
|
|
34
42
|
import { collectorKeyOf } from '../prefab/collector.js';
|
|
@@ -36,6 +44,7 @@ import { RouteAddress } from '../wire/RouteAddress.js';
|
|
|
36
44
|
import { DispatchLifecycle } from '../dispatch/DispatchLifecycle.js';
|
|
37
45
|
import { Dispatcher } from '../dispatch/Dispatcher.js';
|
|
38
46
|
import { LocalRoutePolicy } from '../dispatch/LocalRoutePolicy.js';
|
|
47
|
+
import { servedSurfaces } from '../descriptor/surface.js';
|
|
39
48
|
import { OperationRoute } from '../dispatch/OperationRoute.js';
|
|
40
49
|
import { remoteRoutes } from '../dispatch/remoteRoutes.js';
|
|
41
50
|
import { RouteRegistry } from '../dispatch/RouteRegistry.js';
|
|
@@ -78,726 +87,503 @@ function assertOneOwnerPerKey(
|
|
|
78
87
|
/** Bootstrap a fougere application. */
|
|
79
88
|
export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
80
89
|
const container = options.createContainer();
|
|
81
|
-
// Boot chatter is debug by default; a host (e.g. the CLI) can quiet it.
|
|
82
|
-
const log = new Logger('boot:app');
|
|
83
|
-
|
|
84
|
-
// Builtins — registered under class name (PascalCase) for type-based DI.
|
|
85
|
-
// No level here and none anywhere: a logger consults `setLogLevel`'s value at each
|
|
86
|
-
// emission, so this instance survives a level change and so does every handler that
|
|
87
|
-
// was handed it. A frond declaring `class X extends Logger` takes this key over,
|
|
88
|
-
// like any other port.
|
|
89
|
-
container.registerValue('Logger', new Logger('app'));
|
|
90
|
-
container.register('Config', Config, { lifetime: 'singleton' });
|
|
91
|
-
log.debug('builtins registered (Logger, Config)');
|
|
92
|
-
|
|
93
|
-
// What this app hosts is HANDED IN — stated, scanned, or both (`hostedBy`). Producing
|
|
94
|
-
// it may read a disk; consuming it never does, which is the whole reason this file names
|
|
95
|
-
// no builtin and a Worker can run what it builds.
|
|
96
|
-
const scanStart = performance.now();
|
|
97
|
-
const { fronds, diagnostics } = await hostedBy(options);
|
|
98
|
-
// An app that states nothing AND scans nothing is a mistake — unless something else it
|
|
99
|
-
// declares brings its own entities, which an auth provider does. Refused here and not in
|
|
100
|
-
// `hostedBy`, which is handed the frond sources and cannot see the rest of the app. The
|
|
101
|
-
// condition is the KEYS, not the count: a scan that found nothing is an ordinary answer.
|
|
102
|
-
if (!options.fronds && !options.scan && !options.auth) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
'createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
|
|
105
|
-
+ 'Neither was given, and nothing else declares entities of its own.',
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
const operationModel = resolveEffectiveOperations(fronds, {
|
|
109
|
-
diagnostics,
|
|
110
|
-
remotes: options.remotes,
|
|
111
|
-
adapters: options.adapters,
|
|
112
|
-
});
|
|
113
|
-
const scanMs = (performance.now() - scanStart).toFixed(0);
|
|
114
|
-
const blocking = diagnostics.filter((d) => d.severity === 'blocking');
|
|
115
|
-
log.info(`read ${fronds.length} frond(s) in ${scanMs}ms`
|
|
116
|
-
+ (diagnostics.length ? ` — ${diagnostics.length} thing(s) the scan could not do` : ''));
|
|
117
|
-
|
|
118
|
-
/** Say what could not be read, at the one line everyone already watches. */
|
|
119
|
-
for (const d of blocking) log.error(`[${d.code}] ${d.message}`, d.cause);
|
|
120
|
-
for (const d of diagnostics) if (d.severity === 'warning') log.warn(`[${d.code}] ${d.message}`);
|
|
121
|
-
|
|
122
|
-
/** An ambiguous convention is not a partial scan. */
|
|
123
|
-
const invalidOperations = operationModel.resolutionDiagnostics
|
|
124
|
-
.filter((diagnostic) => diagnostic.severity === 'blocking');
|
|
125
|
-
if (invalidOperations.length > 0) {
|
|
126
|
-
const details = invalidOperations.map((d) =>
|
|
127
|
-
` [${d.code}]${d.subject ? ` ${d.subject}` : ''}\n ${d.message}\n ${d.filePath}`,
|
|
128
|
-
);
|
|
129
|
-
throw new Error(
|
|
130
|
-
`Fougere boot refused: ${invalidOperations.length} unresolved operation contract(s):\n`
|
|
131
|
-
+ details.join('\n'),
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Auth runtime — built once from the lazy AuthConfig produced by a provider factory
|
|
136
|
-
// (e.g. betterAuth({...})) in fougere.config.ts. The provider receives our db +
|
|
137
|
-
// storageFactory so all auth writes flow through Storage.
|
|
138
|
-
let authRuntime: AuthRuntime | undefined;
|
|
139
|
-
if (options.auth) {
|
|
140
|
-
if (!options.storageFactory) {
|
|
141
|
-
throw new Error('createApp: `auth` is set but `storageFactory` is missing — auth providers need it to back their adapter.');
|
|
142
|
-
}
|
|
143
|
-
if (options.db === undefined) {
|
|
144
|
-
throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
|
|
145
|
-
}
|
|
146
|
-
log.info('initializing auth runtime');
|
|
147
|
-
authRuntime = await options.auth.create({
|
|
148
|
-
db: options.db,
|
|
149
|
-
storageFactory: options.storageFactory,
|
|
150
|
-
});
|
|
151
|
-
log.info(`auth ready — mounted at ${authRuntime.basePath}`);
|
|
152
|
-
}
|
|
153
90
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
const remoteRouter = declaredRemotes.length > 0 && options.remoteTransport
|
|
164
|
-
? createRemoteRouter(Object.fromEntries(declaredRemotes), options.remoteTransport)
|
|
165
|
-
: undefined;
|
|
166
|
-
|
|
167
|
-
// What is running on this app — counted at the one door every caller goes through,
|
|
168
|
-
// so releasing it can wait for the work instead of pulling the floor out.
|
|
169
|
-
const inflight = new InFlight();
|
|
170
|
-
|
|
171
|
-
// Middleware storage — read at call time, not at boot time
|
|
172
|
-
const globalMiddlewares: AppMiddleware[] = [];
|
|
173
|
-
const scopedMiddlewares = new Map<string, AppMiddleware[]>();
|
|
174
|
-
/** What this app took on beyond its fronds. Its `up` is the last thing the boot does. */
|
|
175
|
-
const appLifecycle = new AppLifecycle().add(...(options.extensions ?? []));
|
|
176
|
-
const routeRegistry = new RouteRegistry();
|
|
177
|
-
const dispatchLifecycle = new DispatchLifecycle(
|
|
178
|
-
options.dispatchObservers,
|
|
179
|
-
(error, event) => log.error(
|
|
180
|
-
`[dispatch-observer] ${event.stage} ${event.call.address.toString()}`,
|
|
181
|
-
error,
|
|
182
|
-
),
|
|
183
|
-
);
|
|
184
|
-
const dispatcher = new Dispatcher(routeRegistry, inflight, dispatchLifecycle);
|
|
185
|
-
const localDispatcher = new Dispatcher(
|
|
186
|
-
routeRegistry,
|
|
187
|
-
inflight,
|
|
188
|
-
dispatchLifecycle,
|
|
189
|
-
new LocalRoutePolicy((surface) => fronds.servedNames(surface)),
|
|
91
|
+
// Held out here, and not where the ascent reads it, because releasing needs it and
|
|
92
|
+
// releasing has to work from the first line the boot takes something.
|
|
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 ?? []),
|
|
190
100
|
);
|
|
101
|
+
/** The app once it exists — a refusal before that releases the two levels that do. */
|
|
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;
|
|
191
107
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const frond of fronds) {
|
|
219
|
-
// Declared remote: keep the scanned metadata (bridges route with it),
|
|
220
|
-
// register nothing locally — resolve() falls through to the remote façade.
|
|
221
|
-
if (options.remotes && frond.name in options.remotes) {
|
|
222
|
-
log.child(frond.name).info('declared remote — not hosted locally');
|
|
223
|
-
// Its doors answer elsewhere, but what they LISTEN to was read here.
|
|
224
|
-
for (const handler of frond.handlers) {
|
|
225
|
-
const key = facadeKeyOf(handler.address, handler.surface);
|
|
226
|
-
const operations = operationModel.forHandler(handler);
|
|
227
|
-
effectiveByKey.set(key, operations);
|
|
228
|
-
emissions.note(contractsOf(operations), key);
|
|
229
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Everything this app holds, let go in reverse of how it was taken: what an extension took on
|
|
110
|
+
* last, then the container's own, then whoever handed a resource in.
|
|
111
|
+
*/
|
|
112
|
+
const release = async (): Promise<void> => {
|
|
113
|
+
// Every level is told to close even when one refuses, and the refusals leave together —
|
|
114
|
+
// the rule `Lifecycle.down` applies INSIDE its list, applied ACROSS the three. Stated
|
|
115
|
+
// there and broken here, a refusing extension took the container and the connection
|
|
116
|
+
// down with it, which is the leak this gesture exists to prevent.
|
|
117
|
+
const refused: unknown[] = [];
|
|
118
|
+
// Whatever is still held will never reach a destination — the console had it.
|
|
119
|
+
stopAnnouncing?.();
|
|
120
|
+
carry.forget();
|
|
121
|
+
const levels = [
|
|
122
|
+
...(built ? [() => appLifecycle.down(built!)] : []),
|
|
123
|
+
() => container.dispose(),
|
|
124
|
+
() => options.onDispose?.(),
|
|
125
|
+
];
|
|
126
|
+
for (const level of levels) {
|
|
127
|
+
try {
|
|
128
|
+
await level();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Flattened one level: an extension's refusals are already an AggregateError, and
|
|
131
|
+
// nesting them would make the caller unwrap twice to read one list.
|
|
132
|
+
if (error instanceof AggregateError) refused.push(...error.errors);
|
|
133
|
+
else refused.push(error);
|
|
230
134
|
}
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const scope = container.createScope();
|
|
234
|
-
const frondLog = log.child(frond.name);
|
|
235
|
-
|
|
236
|
-
// `reads:` is what makes a cross-source reader exist here, and the list IS its
|
|
237
|
-
// environment — a source holding none of these is never opened. Registered under
|
|
238
|
-
// the type's own name, which is the key `depKeyOf` already derives for a plain
|
|
239
|
-
// parameter: `constructor(private reads: Reads)` and nothing else to say.
|
|
240
|
-
// Declaring `reads:` with nothing to build the reader is a boot that ignores a
|
|
241
|
-
// clause: the handler asking for `Reads` then dies at its first call, on a
|
|
242
|
-
// container message that names neither the clause nor what is missing.
|
|
243
|
-
if (frond.reads?.length && !options.sourcesFactory) {
|
|
244
|
-
frondLog.warn(
|
|
245
|
-
`[reads] ${frond.reads.join(', ')} — declared in frond.config.ts, but this boot passes no `
|
|
246
|
-
+ '`sourcesFactory`, so no reader is registered and a handler asking for `Reads` will fail '
|
|
247
|
-
+ 'at its first call. Pass one (`@fougere/adapter-duckdb`), or drop the clause.',
|
|
248
|
-
);
|
|
249
135
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
// query joins entities from different fronds by definition — `Progress` here,
|
|
253
|
-
// `Book` next door — so restricting the list to its own would make it useless.
|
|
254
|
-
// Naming one IS the authorization; that is what the declaration is for.
|
|
255
|
-
const named = frond.reads
|
|
256
|
-
.map((name) => entityByName.get(lowerFirst(name)))
|
|
257
|
-
.filter((entity): entity is NonNullable<typeof entity> => entity !== undefined);
|
|
258
|
-
if (named.length !== frond.reads.length) {
|
|
259
|
-
const missing = frond.reads.filter((name) => !entityByName.has(lowerFirst(name)));
|
|
260
|
-
frondLog.warn(
|
|
261
|
-
`[reads] ${missing.join(', ')} — named in frond.config.ts but scanned nowhere in this app, `
|
|
262
|
-
+ 'so a query naming one would find no table. Check the spelling, or the entity file.',
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
scope.registerValue('Reads', await options.sourcesFactory(named, frond.name));
|
|
266
|
-
frondLog.debug(`cross-source reader over ${named.length} entit(ies)`);
|
|
136
|
+
if (refused.length > 0) {
|
|
137
|
+
throw new AggregateError(refused, `${refused.length} refusal(s) while releasing the app`);
|
|
267
138
|
}
|
|
139
|
+
};
|
|
268
140
|
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
141
|
+
try {
|
|
142
|
+
// Boot chatter is debug by default; a host (e.g. the CLI) can quiet it.
|
|
143
|
+
const log = new Logger('boot:app', { carry });
|
|
144
|
+
|
|
145
|
+
// Builtins — registered under class name (PascalCase) for type-based DI.
|
|
146
|
+
// No level here and none anywhere: a logger consults `setLogLevel`'s value at each
|
|
147
|
+
// emission, so this instance survives a level change and so does every handler that
|
|
148
|
+
// was handed it. A frond declaring `class X extends Logger` takes this key over,
|
|
149
|
+
// like any other port.
|
|
150
|
+
container.registerValue('Logger', new Logger('app', { carry }));
|
|
151
|
+
container.register('Config', Config, { lifetime: 'singleton' });
|
|
152
|
+
log.debug('builtins registered (Logger, Config)');
|
|
153
|
+
|
|
154
|
+
// What this app hosts is HANDED IN — stated, scanned, or both (`hostedBy`). Producing
|
|
155
|
+
// it may read a disk; consuming it never does, which is the whole reason this file names
|
|
156
|
+
// no builtin and a Worker can run what it builds.
|
|
157
|
+
const scanStart = performance.now();
|
|
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
|
+
);
|
|
167
|
+
// An app that states nothing AND scans nothing is a mistake — unless something else it
|
|
168
|
+
// declares brings its own entities, which an auth provider does. Refused here and not in
|
|
169
|
+
// `hostedBy`, which is handed the frond sources and cannot see the rest of the app. The
|
|
170
|
+
// condition is the KEYS, not the count: a scan that found nothing is an ordinary answer.
|
|
171
|
+
if (!options.fronds && !options.scan && !options.auth && brought.length === 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
|
|
174
|
+
+ 'Neither was given, and nothing else declares entities of its own.',
|
|
175
|
+
);
|
|
286
176
|
}
|
|
287
|
-
|
|
288
|
-
|
|
177
|
+
const operationModel = resolveEffectiveOperations(fronds, {
|
|
178
|
+
diagnostics,
|
|
179
|
+
remotes: options.remotes,
|
|
180
|
+
adapters: options.adapters,
|
|
181
|
+
});
|
|
182
|
+
const scanMs = (performance.now() - scanStart).toFixed(0);
|
|
183
|
+
const blocking = diagnostics.filter((d) => d.severity === 'blocking');
|
|
184
|
+
log.info(`read ${fronds.length} frond(s) in ${scanMs}ms`
|
|
185
|
+
+ (diagnostics.length ? ` — ${diagnostics.length} thing(s) the scan could not do` : ''));
|
|
186
|
+
|
|
187
|
+
/** Say what could not be read, at the one line everyone already watches. */
|
|
188
|
+
for (const d of blocking) log.error(`[${d.code}] ${d.message}`, d.cause);
|
|
189
|
+
for (const d of diagnostics) if (d.severity === 'warning') log.warn(`[${d.code}] ${d.message}`);
|
|
190
|
+
|
|
191
|
+
/** An ambiguous convention is not a partial scan. */
|
|
192
|
+
const invalidOperations = operationModel.resolutionDiagnostics
|
|
193
|
+
.filter((diagnostic) => diagnostic.severity === 'blocking');
|
|
194
|
+
if (invalidOperations.length > 0) {
|
|
195
|
+
const details = invalidOperations.map((d) =>
|
|
196
|
+
` [${d.code}]${d.subject ? ` ${d.subject}` : ''}\n ${d.message}\n ${d.filePath}`,
|
|
197
|
+
);
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Fougere boot refused: ${invalidOperations.length} unresolved operation contract(s):\n`
|
|
200
|
+
+ details.join('\n'),
|
|
201
|
+
);
|
|
289
202
|
}
|
|
290
203
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (declares(entity.entityClass, 'unique') && options.enforces?.(source, 'unique') === false) {
|
|
299
|
-
unenforced.push(`${entity.name} in '${source}'`);
|
|
300
|
-
}
|
|
301
|
-
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
302
|
-
|
|
303
|
-
// Check if the default handler (no surface) declares an output override
|
|
304
|
-
const defaultHandler = frond.handlers.find((h) => h.address === entity.name && !h.surface);
|
|
305
|
-
const outputSchema = defaultHandler?.outputOverride ?? (defaultHandler?.ctor as any)?.__output;
|
|
306
|
-
const scoped = outputSchema && outputSchema !== entity.entityClass
|
|
307
|
-
? baseStorage.output(outputSchema)
|
|
308
|
-
: baseStorage;
|
|
309
|
-
|
|
310
|
-
// Storage is a way out like the client surface — see `StorageGuard`.
|
|
311
|
-
const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name).guard(scoped);
|
|
312
|
-
scope.registerValue(key, guarded);
|
|
313
|
-
|
|
314
|
-
// The default repository IS the guarded port — it already answers every gesture a
|
|
315
|
-
// declared one forwards, so the two forms have the same shape and a handler reads
|
|
316
|
-
// `repo.list()` either way. The wrapper that used to sit here (`{ storage: guarded }`)
|
|
317
|
-
// existed to make `repo.storage` true in both, back when `.storage` was the way in.
|
|
318
|
-
//
|
|
319
|
-
// Not registered for an OWNED entity: an aggregate's members are reached through it
|
|
320
|
-
// and nowhere else, and the default would be a second door under a name a handler
|
|
321
|
-
// can spell. Every member is skipped, not just the one the key is named after —
|
|
322
|
-
// that asymmetry was the whole hole.
|
|
323
|
-
const repoKey = repositoryKeyOf(entity.name);
|
|
324
|
-
const owner = owners.get(entity.name);
|
|
325
|
-
if (owner) {
|
|
326
|
-
frondLog.debug(`${entity.name} — owned by ${owner}, no default repository`);
|
|
327
|
-
} else if (!scope.has(repoKey)) {
|
|
328
|
-
scope.registerValue(repoKey, guarded);
|
|
329
|
-
}
|
|
204
|
+
// Auth runtime — built once from the lazy AuthConfig produced by a provider factory
|
|
205
|
+
// (e.g. betterAuth({...})) in fougere.config.ts. The provider receives our db +
|
|
206
|
+
// storageFactory so all auth writes flow through Storage.
|
|
207
|
+
let authRuntime: AuthRuntime | undefined;
|
|
208
|
+
if (options.auth) {
|
|
209
|
+
if (!options.storageFactory) {
|
|
210
|
+
throw new Error('createApp: `auth` is set but `storageFactory` is missing — auth providers need it to back their adapter.');
|
|
330
211
|
}
|
|
331
|
-
if (
|
|
332
|
-
|
|
212
|
+
if (options.db === undefined) {
|
|
213
|
+
throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
|
|
333
214
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
frondLog.warn(
|
|
338
|
-
`unique declared, and the source does not enforce it: ${unenforced.join(', ')} — `
|
|
339
|
-
+ 'two concurrent writes can both pass',
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Frames, after the ORMs and before anything that may ask for one. A frame is read
|
|
345
|
-
// from the same `deps` every other port is read from — asking for it IS declaring it,
|
|
346
|
-
// so nothing is registered for a frame nobody wants.
|
|
347
|
-
registerFrames(
|
|
348
|
-
scope,
|
|
349
|
-
[...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors].flatMap((d) => d.deps),
|
|
350
|
-
frond.providers,
|
|
351
|
-
{
|
|
352
|
-
entityByName,
|
|
353
|
-
frondOf,
|
|
354
|
-
hostedHere: (name) => !(options.remotes && name in options.remotes),
|
|
215
|
+
log.info('initializing auth runtime');
|
|
216
|
+
authRuntime = await options.auth.create({
|
|
217
|
+
db: options.db,
|
|
355
218
|
storageFactory: options.storageFactory,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
transacted: options.transacted,
|
|
359
|
-
log: frondLog,
|
|
360
|
-
},
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
// Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
|
|
364
|
-
const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
|
|
365
|
-
for (const presenter of frond.presenters) {
|
|
366
|
-
scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
|
|
367
|
-
}
|
|
368
|
-
if (frond.presenters.length > 0) {
|
|
369
|
-
frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
|
|
219
|
+
});
|
|
220
|
+
log.info(`auth ready — mounted at ${authRuntime.basePath}`);
|
|
370
221
|
}
|
|
371
222
|
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
223
|
+
// Remote routing — validated at boot: declaring remotes without a transport is a config error.
|
|
224
|
+
// A remote declaration wins over local presence: `remotes: { blog: url }` IS
|
|
225
|
+
// the topology statement — the frond's code may sit in fronds/**, it runs elsewhere.
|
|
226
|
+
const declaredRemotes = Object.entries(options.remotes ?? {});
|
|
227
|
+
if (declaredRemotes.length > 0 && !options.remoteTransport) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'createApp: `remotes` is declared but `remoteTransport` is missing — pass one (e.g. from @fougere/transport-http).',
|
|
230
|
+
);
|
|
377
231
|
}
|
|
378
|
-
|
|
379
|
-
|
|
232
|
+
const remoteRouter = declaredRemotes.length > 0 && options.remoteTransport
|
|
233
|
+
? createRemoteRouter(Object.fromEntries(declaredRemotes), options.remoteTransport)
|
|
234
|
+
: undefined;
|
|
235
|
+
|
|
236
|
+
// What is running on this app — counted at the one door every caller goes through,
|
|
237
|
+
// so releasing it can wait for the work instead of pulling the floor out.
|
|
238
|
+
const inflight = new InFlight();
|
|
239
|
+
|
|
240
|
+
// Middleware storage — read at call time, not at boot time
|
|
241
|
+
const globalMiddlewares: AppMiddleware[] = [];
|
|
242
|
+
const scopedMiddlewares = new Map<string, AppMiddleware[]>();
|
|
243
|
+
/** What this app took on beyond its fronds. Its `up` is the last thing the boot does. */
|
|
244
|
+
const routeRegistry = new RouteRegistry();
|
|
245
|
+
const dispatchLifecycle = new DispatchLifecycle(
|
|
246
|
+
options.dispatchObservers,
|
|
247
|
+
(error, event) => log.error(
|
|
248
|
+
`[dispatch-observer] ${event.stage} ${event.call.address.toString()}`,
|
|
249
|
+
error,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
const dispatcher = new Dispatcher(routeRegistry, inflight, dispatchLifecycle);
|
|
253
|
+
const localDispatcher = new Dispatcher(
|
|
254
|
+
routeRegistry,
|
|
255
|
+
inflight,
|
|
256
|
+
dispatchLifecycle,
|
|
257
|
+
new LocalRoutePolicy((surface) => fronds.servedNames(surface)),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
function getMiddlewares(entity: string): AppMiddleware[] {
|
|
261
|
+
const scoped = scopedMiddlewares.get(entity) ?? [];
|
|
262
|
+
return [...globalMiddlewares, ...scoped];
|
|
380
263
|
}
|
|
381
264
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
handler: HandlerEntry,
|
|
391
|
-
targetScope: Container,
|
|
392
|
-
facadeKey: string,
|
|
393
|
-
) => {
|
|
394
|
-
if (inheritsCrud(handler.ctor) && !entity) {
|
|
395
|
-
// An installed Crud subject may be absent from the local scan.
|
|
396
|
-
frondLog.debug(`${handler.ctor.name} extends Crud() and no scanned entity is named `
|
|
397
|
-
+ `'${subjectOf(handler.ctor, handler.address)}' — installed entity, or a missing `
|
|
398
|
-
+ `one: no storage will be injected`);
|
|
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;
|
|
399
273
|
}
|
|
274
|
+
const scoped = scopedMiddlewares.get(entity) ?? [];
|
|
275
|
+
scoped.push(middleware);
|
|
276
|
+
scopedMiddlewares.set(entity, scoped);
|
|
277
|
+
}
|
|
400
278
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
candidate.surface === surface && candidate.address === handler.address);
|
|
425
|
-
if (isDeclared && !hasOwnDoor) surfaces.add(surface);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
279
|
+
assertOneOwnerPerKey(fronds, options.remotes);
|
|
280
|
+
|
|
281
|
+
// Every entity of every frond, by name — so a fact can be validated where it LANDS, and
|
|
282
|
+
// so a `reads:` clause can name a neighbour's.
|
|
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);
|
|
289
|
+
// Which frond holds an entity — what turns "a member is remote" into a refusal that
|
|
290
|
+
// names the frond rather than the entity, since `remotes:` is declared per frond.
|
|
291
|
+
const frondOf = new Map(fronds.flatMap((f) => f.entities.map((e) => [e.name, f.name] as const)));
|
|
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
|
+
);
|
|
300
|
+
/** Canonical operation tables, indexed by the same audience key as their facades. */
|
|
301
|
+
const effectiveByKey = new Map<string, EffectiveOperationsMap>();
|
|
428
302
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
entity: handler.address,
|
|
433
|
-
operation,
|
|
434
|
-
...(surface !== undefined ? { surface } : {}),
|
|
435
|
-
});
|
|
436
|
-
routeRegistry.register(new OperationRoute(
|
|
437
|
-
'local',
|
|
438
|
-
address,
|
|
439
|
-
(call) => facade.execute(operation, call.invocation),
|
|
440
|
-
));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
303
|
+
const contractsOf = (operations: EffectiveOperationsMap): OperationsMap => new Map(
|
|
304
|
+
[...operations].map(([name, operation]) => [name, operation as OperationContract] as const),
|
|
305
|
+
);
|
|
443
306
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
307
|
+
// Every port an implementation was bound to, so a `ports:` entry that named none
|
|
308
|
+
// can say so rather than look obeyed.
|
|
309
|
+
const boundPorts = new Set<string>();
|
|
310
|
+
|
|
311
|
+
// Register frond scopes
|
|
312
|
+
// What every frond is installed into, and reads while it is: one container, one route
|
|
313
|
+
// table, one emission list — so what a frond serves is there for the next one to find.
|
|
314
|
+
const assembly: Assembly = {
|
|
315
|
+
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
316
|
+
boundPorts, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
|
|
317
|
+
log, options,
|
|
451
318
|
};
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
},
|
|
464
|
-
}));
|
|
319
|
+
for (const frond of fronds) await installFrond(frond, assembly);
|
|
320
|
+
|
|
321
|
+
// A `ports:` key that matched no port anywhere reads as a choice that was made, and
|
|
322
|
+
// was not. Said once, at the end, because the entry is app-wide while a port is a
|
|
323
|
+
// frond's — no single frond can tell whether a key is a typo or a neighbour's.
|
|
324
|
+
const unused = Object.keys(options.ports ?? {}).filter((port) => !boundPorts.has(port));
|
|
325
|
+
if (unused.length > 0) {
|
|
326
|
+
log.warn(
|
|
327
|
+
`[ports] ${unused.join(', ')} — named in fougere.config.ts, but no scanned class extends `
|
|
328
|
+
+ 'them, so nothing was chosen. Check the spelling, or drop the entry.',
|
|
329
|
+
);
|
|
465
330
|
}
|
|
466
331
|
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
|
|
480
|
-
const entity = frond.entities.find((e) => e.name === subject);
|
|
481
|
-
const facadeKey = facadeKeyOf(handler.address);
|
|
482
|
-
buildFacade(entity, handler, scope, facadeKey);
|
|
483
|
-
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
|
|
484
|
-
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
332
|
+
// Once every door exists: what is announced here and what is listened to are both known.
|
|
333
|
+
emissions.register();
|
|
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);
|
|
485
344
|
}
|
|
486
345
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
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();
|
|
493
355
|
}
|
|
494
356
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// key, which is what a Crud handler asks for, and under the port's own for a holder
|
|
507
|
-
// that legitimately names it. Registering only the latter left a named surface with
|
|
508
|
-
// no door at all once the façade stopped spelling the storage.
|
|
509
|
-
if (entity && options.storageFactory) {
|
|
510
|
-
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
511
|
-
const outputSchema = handler.outputOverride ?? (handler.ctor as any).__output;
|
|
512
|
-
const scoped = outputSchema && outputSchema !== entity.entityClass
|
|
513
|
-
? baseStorage.output(outputSchema)
|
|
514
|
-
: baseStorage;
|
|
515
|
-
// The view is handed over so a filter on a field this door hides is SAID. The
|
|
516
|
-
// guard holds no logger — a warning is the boot's to voice, as a seed's report is.
|
|
517
|
-
const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name, {
|
|
518
|
-
...(outputSchema && outputSchema !== entity.entityClass
|
|
519
|
-
? { view: (outputSchema as { getFields(): Fields }).getFields() }
|
|
520
|
-
: {}),
|
|
521
|
-
outOfView: (message) => frondLog.warn(message),
|
|
522
|
-
}).guard(scoped);
|
|
523
|
-
surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
|
|
524
|
-
surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const facadeKey = facadeKeyOf(handler.address, handler.surface);
|
|
528
|
-
buildFacade(entity, handler, surfaceScope, facadeKey);
|
|
529
|
-
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
|
|
530
|
-
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// A named surface is closed, so what it contains is a fact worth stating.
|
|
534
|
-
// Saying it at boot is the difference between a rule and a rule you can
|
|
535
|
-
// check: an entity you meant to serve and never wrote a handler for is
|
|
536
|
-
// absent HERE, in one line, instead of being discovered missing later.
|
|
537
|
-
const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface as string))].sort();
|
|
538
|
-
for (const surfaceName of surfaceNames) {
|
|
539
|
-
const served = surfaceHandlers
|
|
540
|
-
.filter((h) => h.surface === surfaceName)
|
|
541
|
-
.map((h) => h.address)
|
|
542
|
-
.sort();
|
|
543
|
-
const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
|
|
544
|
-
frondLog.info(
|
|
545
|
-
`surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
|
|
546
|
-
(absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
|
|
357
|
+
/** The last resort, held by the container so every resolution path shares it. */
|
|
358
|
+
container.setFallback?.((name) => {
|
|
359
|
+
if (!remoteRouter) return undefined;
|
|
360
|
+
if (!name.endsWith('Handler') || name.includes(':')) return undefined;
|
|
361
|
+
// Façade-shaped stand-in; routing happens lazily at the first call. Through
|
|
362
|
+
// `lowerFirst` because a DEPENDENCY names the type as written — `ProductHandler`,
|
|
363
|
+
// PascalCase — while a card declares `product`, so the raw strip asked the router for
|
|
364
|
+
// 'Product' and every by-type dependency on a remote handler answered NOT_FOUND.
|
|
365
|
+
return facadeOperations(
|
|
366
|
+
dispatcher,
|
|
367
|
+
lowerFirst(name.replace(/Handler$/, '')),
|
|
547
368
|
);
|
|
548
|
-
}
|
|
369
|
+
});
|
|
549
370
|
|
|
550
|
-
container.registerValue(`frond:${frond.name}`, scope);
|
|
551
|
-
frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
|
|
552
|
-
}
|
|
553
371
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
container.setFallback?.((name) => {
|
|
570
|
-
if (!remoteRouter) return undefined;
|
|
571
|
-
if (!name.endsWith('Handler') || name.includes(':')) return undefined;
|
|
572
|
-
// Façade-shaped stand-in; routing happens lazily at the first call. Through
|
|
573
|
-
// `lowerFirst` because a DEPENDENCY names the type as written — `ProductHandler`,
|
|
574
|
-
// PascalCase — while a card declares `product`, so the raw strip asked the router for
|
|
575
|
-
// 'Product' and every by-type dependency on a remote handler answered NOT_FOUND.
|
|
576
|
-
return facadeOperations(
|
|
577
|
-
dispatcher,
|
|
578
|
-
lowerFirst(name.replace(/Handler$/, '')),
|
|
579
|
-
);
|
|
580
|
-
});
|
|
372
|
+
/** Stop taking calls, and resolve once the ones already running are done. */
|
|
373
|
+
const drain = async (timeoutMs?: number): Promise<void> => {
|
|
374
|
+
inflight.close();
|
|
375
|
+
if (timeoutMs === undefined) return inflight.whenIdle();
|
|
376
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
377
|
+
await Promise.race([
|
|
378
|
+
inflight.whenIdle().then(() => clearTimeout(timer)),
|
|
379
|
+
new Promise<never>((_, reject) => {
|
|
380
|
+
timer = setTimeout(
|
|
381
|
+
() => reject(new Error(`[drain] ${inflight.count} call(s) still running after ${timeoutMs}ms`)),
|
|
382
|
+
timeoutMs,
|
|
383
|
+
);
|
|
384
|
+
}),
|
|
385
|
+
]);
|
|
386
|
+
};
|
|
581
387
|
|
|
582
|
-
|
|
583
|
-
* Everything this app holds, let go in reverse of how it was taken: what an extension took on
|
|
584
|
-
* last, then the container's own, then whoever handed a resource in.
|
|
585
|
-
*/
|
|
586
|
-
const release = async (): Promise<void> => {
|
|
587
|
-
// Every level is told to close even when one refuses, and the refusals leave together —
|
|
588
|
-
// the rule `Lifecycle.down` applies INSIDE its list, applied ACROSS the three. Stated
|
|
589
|
-
// there and broken here, a refusing extension took the container and the connection
|
|
590
|
-
// down with it, which is the leak this gesture exists to prevent.
|
|
591
|
-
const refused: unknown[] = [];
|
|
592
|
-
for (const level of [() => appLifecycle.down(app), () => container.dispose(), () => options.onDispose?.()]) {
|
|
388
|
+
const resolve = <T>(name: string): T => {
|
|
593
389
|
try {
|
|
594
|
-
|
|
595
|
-
} catch (
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
390
|
+
return container.resolve<T>(name);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (name.endsWith('Handler') && !name.includes(':') && !remoteRouter) {
|
|
393
|
+
throw new Error(notLoaded(name.replace(/Handler$/, '')));
|
|
394
|
+
}
|
|
395
|
+
throw err;
|
|
600
396
|
}
|
|
601
|
-
}
|
|
602
|
-
if (refused.length > 0) {
|
|
603
|
-
throw new AggregateError(refused, `${refused.length} refusal(s) while releasing the app`);
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
/** Stop taking calls, and resolve once the ones already running are done. */
|
|
608
|
-
const drain = async (timeoutMs?: number): Promise<void> => {
|
|
609
|
-
inflight.close();
|
|
610
|
-
if (timeoutMs === undefined) return inflight.whenIdle();
|
|
611
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
612
|
-
await Promise.race([
|
|
613
|
-
inflight.whenIdle().then(() => clearTimeout(timer)),
|
|
614
|
-
new Promise<never>((_, reject) => {
|
|
615
|
-
timer = setTimeout(
|
|
616
|
-
() => reject(new Error(`[drain] ${inflight.count} call(s) still running after ${timeoutMs}ms`)),
|
|
617
|
-
timeoutMs,
|
|
618
|
-
);
|
|
619
|
-
}),
|
|
620
|
-
]);
|
|
621
|
-
};
|
|
397
|
+
};
|
|
622
398
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
399
|
+
const schemaFor = async (entity: string): Promise<SchemaView> => {
|
|
400
|
+
const found = fronds.entity(entity);
|
|
401
|
+
if (found) return found.entityClass;
|
|
402
|
+
if (remoteRouter) {
|
|
403
|
+
const route = await remoteRouter.route(entity);
|
|
404
|
+
// A remote door that stores nothing publishes ops and no shape. Saying so beats
|
|
405
|
+
// handing back an empty schema, which would validate every input it was given.
|
|
406
|
+
if (!route.schema) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`'${entity}' is served by frond '${route.frond}' but stores no rows, so it has no schema. `
|
|
409
|
+
+ `Call its operations through the façade instead.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return route.schema;
|
|
629
413
|
}
|
|
630
|
-
throw
|
|
631
|
-
}
|
|
632
|
-
};
|
|
414
|
+
throw new Error(notLoaded(entity));
|
|
415
|
+
};
|
|
633
416
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (!route.schema) {
|
|
642
|
-
throw new Error(
|
|
643
|
-
`'${entity}' is served by frond '${route.frond}' but stores no rows, so it has no schema. `
|
|
644
|
-
+ `Call its operations through the façade instead.`,
|
|
645
|
-
);
|
|
417
|
+
const facadeAt = (key: string, topology: boolean): Record<string, Function> | undefined => {
|
|
418
|
+
try {
|
|
419
|
+
return topology
|
|
420
|
+
? resolve<Record<string, Function>>(key)
|
|
421
|
+
: container.resolve<Record<string, Function>>(key);
|
|
422
|
+
} catch {
|
|
423
|
+
return undefined;
|
|
646
424
|
}
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
throw new Error(notLoaded(entity));
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
const facadeAt = (key: string, topology: boolean): Record<string, Function> | undefined => {
|
|
653
|
-
try {
|
|
654
|
-
return topology
|
|
655
|
-
? resolve<Record<string, Function>>(key)
|
|
656
|
-
: container.resolve<Record<string, Function>>(key);
|
|
657
|
-
} catch {
|
|
658
|
-
return undefined;
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
/** THE membership rule, stated once — every projection reads this and nothing else. */
|
|
663
|
-
const facadeFor = (entity: string, surface?: string): Record<string, Function> | undefined => {
|
|
664
|
-
if (!surface) return facadeAt(facadeKeyOf(entity), true);
|
|
665
|
-
|
|
666
|
-
const own = facadeAt(facadeKeyOf(entity, surface), false);
|
|
667
|
-
const declared = fronds.owner(entity)?.surfaces?.[surface];
|
|
668
|
-
if (!declared) return own;
|
|
669
|
-
if (!declared.some((n) => n.toLowerCase() === entity.toLowerCase())) return undefined;
|
|
670
|
-
if (own) return own;
|
|
671
|
-
|
|
672
|
-
const fallback = facadeAt(facadeKeyOf(entity), false);
|
|
673
|
-
return fallback
|
|
674
|
-
? facadeOperations(
|
|
675
|
-
localDispatcher,
|
|
676
|
-
entity,
|
|
677
|
-
routeRegistry.operationNames(entity, surface),
|
|
678
|
-
surface,
|
|
679
|
-
)
|
|
680
|
-
: undefined;
|
|
681
|
-
};
|
|
425
|
+
};
|
|
682
426
|
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
427
|
+
/** Said once per pair, so a door that registers in a loop says it once. */
|
|
428
|
+
const saidAbsent = new Set<string>();
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A surface is declared in the frond that serves it. When that frond runs in another
|
|
432
|
+
* process, this one never asked for its doors, and answering 'no' is the only thing a
|
|
433
|
+
* synchronous rule can do — so it says so rather than registering nothing in silence.
|
|
434
|
+
*/
|
|
435
|
+
const sayNoSurfaceAcross = (entity: string, surface: string): void => {
|
|
436
|
+
if (!remoteRouter || saidAbsent.has(`${surface}:${entity}`)) return;
|
|
437
|
+
saidAbsent.add(`${surface}:${entity}`);
|
|
438
|
+
log.warn(
|
|
439
|
+
`surface '${surface}' serves nothing for '${entity}' — the frond that declares it runs `
|
|
440
|
+
+ 'elsewhere, and a remote is asked for its doors at the first call, not at boot. '
|
|
441
|
+
+ 'The default door answers.',
|
|
442
|
+
);
|
|
443
|
+
};
|
|
693
444
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
445
|
+
/** THE membership rule, stated once — every projection reads this and nothing else. */
|
|
446
|
+
const facadeFor = (entity: string, surface?: string): Record<string, Function> | undefined => {
|
|
447
|
+
if (!surface) return facadeAt(facadeKeyOf(entity), true);
|
|
697
448
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
};
|
|
449
|
+
const own = facadeAt(facadeKeyOf(entity, surface), false);
|
|
450
|
+
const owner = fronds.owner(entity);
|
|
451
|
+
if (!owner) {
|
|
452
|
+
sayNoSurfaceAcross(entity, surface);
|
|
453
|
+
return own;
|
|
454
|
+
}
|
|
705
455
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
456
|
+
const declared = owner.surfaces?.[surface];
|
|
457
|
+
if (!declared) return own;
|
|
458
|
+
if (!declared.some((n) => n.toLowerCase() === entity.toLowerCase())) return undefined;
|
|
459
|
+
if (own) return own;
|
|
460
|
+
|
|
461
|
+
const fallback = facadeAt(facadeKeyOf(entity), false);
|
|
462
|
+
return fallback
|
|
463
|
+
? facadeOperations(
|
|
464
|
+
localDispatcher,
|
|
465
|
+
entity,
|
|
466
|
+
routeRegistry.operationNames(entity, surface),
|
|
467
|
+
surface,
|
|
468
|
+
)
|
|
469
|
+
: undefined;
|
|
470
|
+
};
|
|
713
471
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
472
|
+
if (remoteRouter) {
|
|
473
|
+
const remoteFacades = new Map<string, Record<string, Function>>();
|
|
474
|
+
routeRegistry.addResolver(remoteRoutes((entity) => {
|
|
475
|
+
const known = remoteFacades.get(entity);
|
|
476
|
+
if (known) return known;
|
|
477
|
+
const facade = createRemoteFacade(entity, remoteRouter, getMiddlewares);
|
|
478
|
+
remoteFacades.set(entity, facade);
|
|
479
|
+
return facade;
|
|
480
|
+
}));
|
|
719
481
|
}
|
|
720
|
-
};
|
|
721
482
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (!owner) return undefined;
|
|
483
|
+
/** The terms beside a door, with the exact same named-surface fallback rule. */
|
|
484
|
+
const operationsFor = (entity: string, surface?: string): EffectiveOperationsMap | undefined => {
|
|
485
|
+
if (!surface) return effectiveByKey.get(facadeKeyOf(entity));
|
|
726
486
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
return
|
|
731
|
-
|
|
732
|
-
|
|
487
|
+
const own = effectiveByKey.get(facadeKeyOf(entity, surface));
|
|
488
|
+
const declared = fronds.owner(entity)?.surfaces?.[surface];
|
|
489
|
+
if (!declared) return own;
|
|
490
|
+
return declared.some((name) => name.toLowerCase() === entity.toLowerCase())
|
|
491
|
+
? (own ?? effectiveByKey.get(facadeKeyOf(entity)))
|
|
492
|
+
: undefined;
|
|
493
|
+
};
|
|
733
494
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
schemaFor,
|
|
748
|
-
facadeFor,
|
|
749
|
-
operationsFor,
|
|
750
|
-
listensTo: () => emissions.listensTo(),
|
|
751
|
-
deliver: (fact, payload) => emissions.deliver(fact, payload),
|
|
752
|
-
storageFor,
|
|
753
|
-
presenterFor,
|
|
754
|
-
dispose: release,
|
|
755
|
-
drain,
|
|
756
|
-
inFlight: () => inflight.count,
|
|
757
|
-
[Symbol.asyncDispose]: release,
|
|
758
|
-
serveRpc(op: string, answer: RpcAnswer): void {
|
|
759
|
-
// Refused rather than replaced: two declarations of one name would make the answer
|
|
760
|
-
// depend on wiring order, and `discover` is in here precisely so it cannot be taken.
|
|
761
|
-
const address = new RouteAddress({ entity: 'rpc', operation: op });
|
|
762
|
-
if (routeRegistry.find(address)) {
|
|
763
|
-
throw new Error(`rpc operation '${op}' is already served; a second declaration would depend on wiring order`);
|
|
495
|
+
/**
|
|
496
|
+
* The storage an entity is backed by — the dual of `facadeFor`, which serves its client-facing
|
|
497
|
+
* door.
|
|
498
|
+
*/
|
|
499
|
+
const storageFor = (entity: string): unknown | undefined => {
|
|
500
|
+
const owner = fronds.owner(entity);
|
|
501
|
+
if (!owner) return undefined;
|
|
502
|
+
|
|
503
|
+
const key = storageKeyOf(entity);
|
|
504
|
+
try {
|
|
505
|
+
return container.resolve<Container>(`frond:${owner.name}`).resolve(key);
|
|
506
|
+
} catch {
|
|
507
|
+
return undefined;
|
|
764
508
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
)
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (typeof args[0] === 'string') {
|
|
777
|
-
const [entity, mw] = args as [string, AppMiddleware];
|
|
778
|
-
const list = scopedMiddlewares.get(entity) ?? [];
|
|
779
|
-
list.push(mw);
|
|
780
|
-
scopedMiddlewares.set(entity, list);
|
|
781
|
-
} else {
|
|
782
|
-
globalMiddlewares.push(args[0] as AppMiddleware);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/** The presenter of an entity, resolved through its owning frond's scope. */
|
|
512
|
+
const presenterFor = (entity: string): unknown | undefined => {
|
|
513
|
+
const owner = fronds.owner(entity);
|
|
514
|
+
if (!owner) return undefined;
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
return container.resolve<Container>(`frond:${owner.name}`).resolve(presenterKeyOf(entity));
|
|
518
|
+
} catch {
|
|
519
|
+
return undefined;
|
|
783
520
|
}
|
|
784
|
-
}
|
|
785
|
-
auth: authRuntime,
|
|
786
|
-
};
|
|
521
|
+
};
|
|
787
522
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
523
|
+
const app: App = {
|
|
524
|
+
container,
|
|
525
|
+
fronds,
|
|
526
|
+
// What this app publishes, straight from fougere.config.ts — the doors read it,
|
|
527
|
+
// so an undeclared adapter serves nothing whatever a host mounted.
|
|
528
|
+
adapters: options.adapters ?? {},
|
|
529
|
+
// Where a call goes, as DECLARED. Kept because a reader needs it beside what the
|
|
530
|
+
// runtime OBSERVED — `rpc.topology` calls a frond remote because it answered, never
|
|
531
|
+
// because a key said so, and the two disagree exactly when something is misconfigured.
|
|
532
|
+
remotes: Object.freeze({ ...(options.remotes ?? {}) }),
|
|
533
|
+
dispatch: (call) => dispatcher.dispatch(call),
|
|
534
|
+
local: localDispatcher,
|
|
535
|
+
resolve,
|
|
536
|
+
schemaFor,
|
|
537
|
+
facadeFor,
|
|
538
|
+
operationsFor,
|
|
539
|
+
listensTo: () => emissions.listensTo(),
|
|
540
|
+
deliver: (fact, payload) => emissions.deliver(fact, payload),
|
|
541
|
+
storageFor,
|
|
542
|
+
presenterFor,
|
|
543
|
+
dispose: release,
|
|
544
|
+
drain,
|
|
545
|
+
inFlight: () => inflight.count,
|
|
546
|
+
[Symbol.asyncDispose]: release,
|
|
547
|
+
serveRpc(op: string, answer: RpcAnswer): void {
|
|
548
|
+
// Refused rather than replaced: two declarations of one name would make the answer
|
|
549
|
+
// depend on wiring order, and `discover` is in here precisely so it cannot be taken.
|
|
550
|
+
const address = new RouteAddress({ entity: 'rpc', operation: op });
|
|
551
|
+
if (routeRegistry.find(address)) {
|
|
552
|
+
throw new Error(`rpc operation '${op}' is already served; a second declaration would depend on wiring order`);
|
|
553
|
+
}
|
|
554
|
+
routeRegistry.register(new OperationRoute(
|
|
555
|
+
'system',
|
|
556
|
+
address,
|
|
557
|
+
(call) => answer(call.invocation, call.address.surface),
|
|
558
|
+
));
|
|
559
|
+
},
|
|
560
|
+
extensions: () => appLifecycle.names(),
|
|
561
|
+
observe(observer) {
|
|
562
|
+
return dispatchLifecycle.add(observer);
|
|
563
|
+
},
|
|
564
|
+
use(...args: [AppMiddleware] | [string, AppMiddleware]): void {
|
|
565
|
+
return typeof args[0] === 'string'
|
|
566
|
+
? use(args[1] as AppMiddleware, args[0])
|
|
567
|
+
: use(args[0] as AppMiddleware);
|
|
568
|
+
},
|
|
569
|
+
auth: authRuntime,
|
|
570
|
+
};
|
|
791
571
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
572
|
+
// The card is an rpc op like any other, so one registry answers and one refusal names
|
|
573
|
+
// what is served. A package's op is declared the same way, from outside.
|
|
574
|
+
app.serveRpc('discover', (_invocation, surface) => identityCardOf(app, surface));
|
|
575
|
+
|
|
576
|
+
// The last thing the boot does, and the first thing a release undoes. An extension may
|
|
577
|
+
// await here — which is what a provider needing to OPEN something could never do.
|
|
578
|
+
built = app;
|
|
799
579
|
await appLifecycle.up(app);
|
|
580
|
+
|
|
581
|
+
return app;
|
|
800
582
|
} catch (cause) {
|
|
583
|
+
// The caller cannot release what a failed boot took: it handed `onDispose` over before
|
|
584
|
+
// this call and never receives the app that would carry it back. Whoever opened a
|
|
585
|
+
// connection to give us would otherwise leak it on every refusal — a source, a scan
|
|
586
|
+
// that threw, a port bound twice, and not only an extension that refused to rise.
|
|
801
587
|
try {
|
|
802
588
|
await release();
|
|
803
589
|
} catch (refused) {
|
|
@@ -808,6 +594,4 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
808
594
|
}
|
|
809
595
|
throw cause;
|
|
810
596
|
}
|
|
811
|
-
|
|
812
|
-
return app;
|
|
813
|
-
}
|
|
597
|
+
}
|