@fougere/core 0.12.1-alpha.0 → 0.12.4-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/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/DomainCluster.d.ts.map +1 -1
- package/dist/DomainCluster.js +1 -1
- package/dist/DomainCluster.js.map +1 -1
- package/dist/EffectiveOperationModel.d.ts +1 -4
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +182 -173
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +5 -3
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +11 -13
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +1 -5
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/Source.js +3 -3
- package/dist/Source.js.map +1 -1
- package/dist/SourceView.d.ts +0 -2
- package/dist/SourceView.d.ts.map +1 -1
- package/dist/boot/App.d.ts +2 -4
- package/dist/boot/App.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.d.ts +5 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +19 -10
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/AuthRuntime.d.ts +5 -12
- package/dist/boot/AuthRuntime.d.ts.map +1 -1
- package/dist/boot/AuthRuntime.js +2 -1
- package/dist/boot/AuthRuntime.js.map +1 -1
- package/dist/boot/CreateAppOptions.d.ts +0 -11
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +3 -6
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/Hosting.d.ts +2 -1
- package/dist/boot/Hosting.d.ts.map +1 -1
- package/dist/boot/ambient.als.d.ts.map +1 -1
- package/dist/boot/ambient.als.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/arity.d.ts +13 -0
- package/dist/boot/arity.d.ts.map +1 -0
- package/dist/boot/arity.js +33 -0
- package/dist/boot/arity.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +143 -265
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +105 -115
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/relations.d.ts +10 -0
- package/dist/boot/relations.d.ts.map +1 -1
- package/dist/boot/relations.js +35 -2
- package/dist/boot/relations.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +1 -1
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +6 -0
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js +19 -0
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/builtin/Carry.d.ts.map +1 -1
- package/dist/builtin/Carry.js.map +1 -1
- package/dist/builtin/Logger.d.ts.map +1 -1
- package/dist/builtin/Logger.js +3 -1
- package/dist/builtin/Logger.js.map +1 -1
- package/dist/builtin/config.js.map +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/crypto/node.d.ts.map +1 -1
- package/dist/crypto/node.js.map +1 -1
- package/dist/crypto/webcrypto.d.ts.map +1 -1
- package/dist/crypto/webcrypto.js.map +1 -1
- package/dist/descriptor/Fronds.d.ts +5 -0
- package/dist/descriptor/Fronds.d.ts.map +1 -1
- package/dist/descriptor/Fronds.js +8 -0
- package/dist/descriptor/Fronds.js.map +1 -1
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.d.ts.map +1 -1
- package/dist/descriptor/index.js +1 -0
- package/dist/descriptor/index.js.map +1 -1
- package/dist/dispatch/ArrayResult.d.ts.map +1 -1
- package/dist/dispatch/ArrayResult.js.map +1 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +11 -3
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts +3 -2
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js +11 -1
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
- package/dist/dispatch/RouteRegistry.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +4 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/storage/Comparison.d.ts +2 -2
- package/dist/storage/Comparison.d.ts.map +1 -1
- package/dist/storage/Comparison.js +3 -1
- package/dist/storage/Comparison.js.map +1 -1
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +1 -1
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/Emit.d.ts +0 -2
- package/dist/wire/Emit.d.ts.map +1 -1
- package/dist/wire/Emit.js +1 -1
- package/dist/wire/Emit.js.map +1 -1
- package/dist/wire/Facade.d.ts +7 -0
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js +13 -0
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.d.ts +0 -4
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/OperationsMap.d.ts +0 -3
- package/dist/wire/OperationsMap.d.ts.map +1 -1
- package/dist/wire/OperationsMap.js +0 -5
- package/dist/wire/OperationsMap.js.map +1 -1
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/IdentityCard.d.ts.map +1 -1
- package/dist/wire/card/IdentityCard.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js.map +1 -1
- package/dist/wire/http-error.d.ts.map +1 -1
- package/dist/wire/http-error.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +5 -5
- package/src/CallIdentity.ts +6 -0
- package/src/DomainCluster.ts +2 -1
- package/src/EffectiveOperationModel.ts +236 -198
- package/src/FougereConfig.ts +17 -16
- package/src/FrondConfig.ts +2 -0
- package/src/FrondDeclaration.ts +1 -7
- package/src/Source.ts +3 -3
- package/src/SourceView.ts +0 -2
- package/src/boot/App.ts +2 -4
- package/src/boot/AppLifecycle.ts +22 -10
- package/src/boot/AuthRuntime.ts +6 -13
- package/src/boot/CreateAppOptions.ts +0 -11
- package/src/boot/Emissions.ts +7 -7
- package/src/boot/Hosting.ts +2 -1
- package/src/boot/ambient.als.ts +2 -0
- package/src/boot/apply.ts +1 -0
- package/src/boot/arity.ts +42 -0
- package/src/boot/bootstrap.ts +175 -300
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +123 -146
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +42 -10
- package/src/boot/remote.ts +1 -0
- package/src/boot/seed.ts +3 -5
- package/src/boot/statement-drift.ts +25 -0
- package/src/boot/together.ts +3 -0
- package/src/builtin/Carry.ts +1 -0
- package/src/builtin/Logger.ts +9 -1
- package/src/builtin/config.ts +1 -0
- package/src/contract.ts +1 -0
- package/src/crypto/encoding.ts +4 -0
- package/src/crypto/node.ts +2 -0
- package/src/crypto/webcrypto.ts +2 -0
- package/src/descriptor/Fronds.ts +12 -0
- package/src/descriptor/index.ts +1 -0
- package/src/dispatch/ArrayResult.ts +1 -0
- package/src/dispatch/Dispatcher.ts +1 -0
- package/src/dispatch/HandlerFacade.ts +16 -4
- package/src/dispatch/InFlight.ts +18 -2
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +2 -4
- package/src/loader.ts +1 -0
- package/src/node.ts +1 -1
- package/src/prefab/CrudConstructor.ts +1 -11
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Comparison.ts +7 -4
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +9 -1
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +2 -0
- package/src/wire/Emit.ts +1 -1
- package/src/wire/Facade.ts +17 -10
- package/src/wire/FougereError.ts +2 -0
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/OperationsMap.ts +0 -10
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/binding.ts +1 -0
- package/src/wire/card/IdentityCard.ts +1 -0
- package/src/wire/drift.ts +3 -0
- package/src/wire/http-error.ts +2 -0
- package/src/wire/loggerMiddleware.ts +1 -0
- package/dist/boot/AuthConfig.d.ts +0 -14
- package/dist/boot/AuthConfig.d.ts.map +0 -1
- package/dist/boot/AuthConfig.js +0 -2
- package/dist/boot/AuthConfig.js.map +0 -1
- package/dist/boot/AuthContext.d.ts +0 -12
- package/dist/boot/AuthContext.d.ts.map +0 -1
- package/dist/boot/AuthContext.js +0 -2
- package/dist/boot/AuthContext.js.map +0 -1
- package/dist/storage/ComparisonName.d.ts +0 -3
- package/dist/storage/ComparisonName.d.ts.map +0 -1
- package/dist/storage/ComparisonName.js +0 -2
- package/dist/storage/ComparisonName.js.map +0 -1
- package/src/boot/AuthConfig.ts +0 -14
- package/src/boot/AuthContext.ts +0 -12
- package/src/storage/ComparisonName.ts +0 -3
package/src/boot/bootstrap.ts
CHANGED
|
@@ -7,15 +7,15 @@ import { hostedBy } from './hosted.js';
|
|
|
7
7
|
import { nestingOf, parentsFirst } from './nesting.js';
|
|
8
8
|
import type { Fronds } from '../descriptor/Fronds.js';
|
|
9
9
|
import { installFrond, type Assembly } from './install.js';
|
|
10
|
-
import { dependentsOf, releasing, unfinishable, unheldAmong } from './relations.js';
|
|
10
|
+
import { dependentsOf, releasing, unfinishable, unheldAmong, unpaired } from './relations.js';
|
|
11
11
|
import { release as releaseRow } from '../dispatch/Release.js';
|
|
12
12
|
import type { Hosting } from './Hosting.js';
|
|
13
13
|
import { peerOver } from './peerOver.js';
|
|
14
14
|
import { JOURNAL, type Journal } from '../dispatch/Journal.js';
|
|
15
15
|
import type { RelationCheck } from '../dispatch/RelationCheck.js';
|
|
16
16
|
import { refusalOf, type Diagnostic } from '../diagnostic.js';
|
|
17
|
-
import type { AuthRuntime } from './AuthRuntime.js';
|
|
18
17
|
import type { App } from './App.js';
|
|
18
|
+
import type { Extension } from './Extension.js';
|
|
19
19
|
import type { CreateAppOptions } from './CreateAppOptions.js';
|
|
20
20
|
import type { AppMiddleware } from '../wire/AppMiddleware.js';
|
|
21
21
|
import { Carry } from '../builtin/Carry.js';
|
|
@@ -24,8 +24,6 @@ import type { LogRecord } from '../builtin/LogRecord.js';
|
|
|
24
24
|
import LogLine, { CARRIES_LINE } from '../builtin/LogLine.js';
|
|
25
25
|
import { emitKeyOf, type Emit } from '../wire/Emit.js';
|
|
26
26
|
|
|
27
|
-
/** The fact the boot announces, spelled once. */
|
|
28
|
-
const LOG_LINE = lowerFirst(LogLine.name);
|
|
29
27
|
import { Config } from '../builtin/config.js';
|
|
30
28
|
import { createRemoteRouter, createRemoteFacade, type RemoteRouter } from './remote.js';
|
|
31
29
|
import type { Peer } from './Peer.js';
|
|
@@ -38,15 +36,14 @@ import { resolveEffectiveOperations, type EffectiveOperationModel } from '../Eff
|
|
|
38
36
|
import { type EffectiveOperationsMap } from '../EffectiveOperationsMap.js';
|
|
39
37
|
|
|
40
38
|
import { InFlight } from '../dispatch/InFlight.js';
|
|
41
|
-
|
|
42
|
-
import { RPC_ENTITY, type RpcAnswer } from '../wire/RpcAnswer.js';
|
|
39
|
+
import { RPC_ENTITY } from '../wire/RpcAnswer.js';
|
|
43
40
|
import { Invocation } from '../wire/Invocation.js';
|
|
44
|
-
import { facadeKeyOf } from '../wire/Facade.js';
|
|
41
|
+
import { addressOf, facadeKeyOf, isFacadeKey } from '../wire/Facade.js';
|
|
45
42
|
import { identityCardOf } from './card.js';
|
|
46
|
-
import { AppLifecycle, migrating } from './AppLifecycle.js';
|
|
43
|
+
import { AppLifecycle, closeAll, migrating } from './AppLifecycle.js';
|
|
47
44
|
import { seeding } from './seed.js';
|
|
48
45
|
|
|
49
|
-
import { storageKeyOf } from '../storage/Storage.js';
|
|
46
|
+
import { storageKeyOf, type Storage } from '../storage/Storage.js';
|
|
50
47
|
|
|
51
48
|
import { presenterKeyOf } from '../prefab/presenter.js';
|
|
52
49
|
|
|
@@ -60,6 +57,8 @@ import { remoteRoutes } from '../dispatch/remoteRoutes.js';
|
|
|
60
57
|
import { RouteRegistry } from '../dispatch/RouteRegistry.js';
|
|
61
58
|
import { facadeOperations } from '../entry/facade.js';
|
|
62
59
|
|
|
60
|
+
const LOG_LINE = lowerFirst(LogLine.name);
|
|
61
|
+
|
|
63
62
|
/** The one wording for "nobody hosts this here", with both ways out. */
|
|
64
63
|
const notLoaded = (entity: string) =>
|
|
65
64
|
`Frond for '${entity}' is not loaded.\n` +
|
|
@@ -123,11 +122,11 @@ async function readFronds(
|
|
|
123
122
|
const { fronds, diagnostics } = await hostedBy(
|
|
124
123
|
brought.length > 0 ? { ...options, fronds: [...(options.fronds ?? []), ...brought] } : options,
|
|
125
124
|
);
|
|
126
|
-
// An app that states nothing AND scans nothing is a mistake — unless
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
if (!options.fronds && !options.scan &&
|
|
125
|
+
// An app that states nothing AND scans nothing is a mistake — unless an extension brings
|
|
126
|
+
// fronds of its own. Refused here and not in `hostedBy`, which is handed the frond sources
|
|
127
|
+
// and cannot see the rest of the app. The condition is the KEYS, not the count: a scan that
|
|
128
|
+
// found nothing is an ordinary answer.
|
|
129
|
+
if (!options.fronds && !options.scan && brought.length === 0) {
|
|
131
130
|
throw new Error(
|
|
132
131
|
'createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
|
|
133
132
|
+ 'Neither was given, and nothing else declares entities of its own.\n'
|
|
@@ -175,14 +174,14 @@ async function readFronds(
|
|
|
175
174
|
function serveCoreRpc(
|
|
176
175
|
app: App,
|
|
177
176
|
hosting: Hosting,
|
|
178
|
-
storageFor: (entity: string) =>
|
|
177
|
+
storageFor: (entity: string) => Storage | undefined,
|
|
179
178
|
): void {
|
|
180
179
|
app.serveRpc('discover', (_invocation, surface) => identityCardOf(app, surface));
|
|
181
180
|
|
|
182
181
|
app.serveRpc('holds', async (invocation) => {
|
|
183
182
|
const named = String(invocation.params.entity);
|
|
184
183
|
const keys = (invocation.params.keys ?? []) as readonly unknown[];
|
|
185
|
-
const rows = storageFor(named)
|
|
184
|
+
const rows = storageFor(named);
|
|
186
185
|
if (!rows) return { missing: [] };
|
|
187
186
|
const found = await rows.findByKeys(keys.map(String));
|
|
188
187
|
|
|
@@ -239,22 +238,14 @@ function peerBehind(entity: string, router: RemoteRouter): Peer {
|
|
|
239
238
|
};
|
|
240
239
|
}
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
241
|
+
function ownedBy<T>(fronds: Fronds, container: Container, entity: string, key: string): T | undefined {
|
|
242
|
+
const owner = fronds.owner(entity);
|
|
243
|
+
const scopeKey = `frond:${owner?.name}`;
|
|
244
|
+
if (!owner || !container.has(scopeKey)) return undefined;
|
|
246
245
|
|
|
247
|
-
|
|
246
|
+
const scope = container.resolve<Container>(scopeKey);
|
|
248
247
|
|
|
249
|
-
|
|
250
|
-
inflight.whenIdle().then(() => clearTimeout(timer)),
|
|
251
|
-
new Promise<never>((_, reject) => {
|
|
252
|
-
timer = setTimeout(
|
|
253
|
-
() => reject(new Error(`[drain] ${inflight.count} call(s) still running after ${timeoutMs}ms`)),
|
|
254
|
-
timeoutMs,
|
|
255
|
-
);
|
|
256
|
-
}),
|
|
257
|
-
]);
|
|
248
|
+
return scope.has(key) ? scope.resolve<T>(key) : undefined;
|
|
258
249
|
}
|
|
259
250
|
|
|
260
251
|
/** What a call goes through in this process, before any frond is installed into it. */
|
|
@@ -284,7 +275,7 @@ function dispatching(
|
|
|
284
275
|
const scopedMiddlewares = new Map<string, AppMiddleware[]>();
|
|
285
276
|
const routeRegistry = new RouteRegistry();
|
|
286
277
|
const dispatchLifecycle = new DispatchLifecycle(
|
|
287
|
-
|
|
278
|
+
[],
|
|
288
279
|
(error, event) => log.error(
|
|
289
280
|
`[dispatch-observer] ${event.stage} ${event.call.address.toString()}`,
|
|
290
281
|
error,
|
|
@@ -354,15 +345,14 @@ interface Hosted {
|
|
|
354
345
|
fronds: Fronds;
|
|
355
346
|
container: Container;
|
|
356
347
|
options: CreateAppOptions;
|
|
357
|
-
declaredRemotes: [string, string][];
|
|
358
348
|
remoteRouter: RemoteRouter | undefined;
|
|
359
349
|
entityByName: Map<string, SchemaView>;
|
|
360
350
|
frondOf: Map<string, string>;
|
|
361
|
-
storageOf: (entity: string) =>
|
|
351
|
+
storageOf: (entity: string) => Storage | undefined;
|
|
362
352
|
}
|
|
363
353
|
|
|
364
354
|
function hostingFor(
|
|
365
|
-
{ fronds, container, options,
|
|
355
|
+
{ fronds, container, options, remoteRouter, entityByName, frondOf, storageOf }: Hosted,
|
|
366
356
|
): { hosting: Hosting; journalOf: () => Journal | undefined } {
|
|
367
357
|
// What carries a release writes none: an instrumentation frond's own rows are kept while a
|
|
368
358
|
// release happens, and journalling them would begin one inside the one being written down.
|
|
@@ -396,7 +386,7 @@ function hostingFor(
|
|
|
396
386
|
// off a card, while the question here is asked of a PROCESS about rows it may be alone in
|
|
397
387
|
// knowing about. Nothing is cached — a peer that was down at boot answers the next call.
|
|
398
388
|
peers: () => (options.remoteTransport
|
|
399
|
-
?
|
|
389
|
+
? Object.values(options.remotes ?? {}).map((url) => peerOver(options.remoteTransport!(url)))
|
|
400
390
|
: []),
|
|
401
391
|
peerOf: (entity) => (remoteRouter ? peerBehind(entity, remoteRouter) : undefined),
|
|
402
392
|
};
|
|
@@ -423,14 +413,11 @@ interface Serving {
|
|
|
423
413
|
function readings(
|
|
424
414
|
{ container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log }: Serving,
|
|
425
415
|
): Pick<App, 'resolve' | 'schemaFor' | 'facadeFor' | 'operationsFor' | 'presenterFor'> {
|
|
426
|
-
/** Stop taking calls, and resolve once the ones already running are done. */
|
|
427
416
|
const resolve = <T>(name: string): T => {
|
|
428
417
|
try {
|
|
429
418
|
return container.resolve<T>(name);
|
|
430
419
|
} catch (err) {
|
|
431
|
-
if (name
|
|
432
|
-
throw new Error(notLoaded(name.replace(/Handler$/, '')));
|
|
433
|
-
}
|
|
420
|
+
if (isFacadeKey(name) && !remoteRouter) throw new Error(notLoaded(addressOf(name)));
|
|
434
421
|
throw err;
|
|
435
422
|
}
|
|
436
423
|
};
|
|
@@ -448,6 +435,7 @@ function readings(
|
|
|
448
435
|
+ `Call its operations through the façade instead.`,
|
|
449
436
|
);
|
|
450
437
|
}
|
|
438
|
+
|
|
451
439
|
return route.schema;
|
|
452
440
|
}
|
|
453
441
|
throw new Error(notLoaded(entity));
|
|
@@ -486,18 +474,18 @@ function readings(
|
|
|
486
474
|
if (!surface) return facadeAt(facadeKeyOf(entity), true);
|
|
487
475
|
|
|
488
476
|
const own = facadeAt(facadeKeyOf(entity, surface), false);
|
|
489
|
-
|
|
490
|
-
if (!owner) {
|
|
477
|
+
if (!fronds.owner(entity)) {
|
|
491
478
|
sayNoSurfaceAcross(entity, surface);
|
|
479
|
+
|
|
492
480
|
return own;
|
|
493
481
|
}
|
|
494
482
|
|
|
495
|
-
const
|
|
496
|
-
if (
|
|
497
|
-
if (
|
|
498
|
-
if (own) return own;
|
|
483
|
+
const admitted = fronds.admits(surface, entity);
|
|
484
|
+
if (admitted === false) return undefined;
|
|
485
|
+
if (own || admitted === undefined) return own;
|
|
499
486
|
|
|
500
487
|
const fallback = facadeAt(facadeKeyOf(entity), false);
|
|
488
|
+
|
|
501
489
|
return fallback
|
|
502
490
|
? facadeOperations(
|
|
503
491
|
localDispatcher,
|
|
@@ -508,189 +496,178 @@ function readings(
|
|
|
508
496
|
: undefined;
|
|
509
497
|
};
|
|
510
498
|
|
|
511
|
-
/** The terms beside a facade, with the exact same named-surface fallback rule. */
|
|
512
499
|
const operationsFor = (entity: string, surface?: string): EffectiveOperationsMap | undefined => {
|
|
513
500
|
if (!surface) return effectiveByKey.get(facadeKeyOf(entity));
|
|
514
501
|
|
|
515
502
|
const own = effectiveByKey.get(facadeKeyOf(entity, surface));
|
|
516
|
-
const
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
: undefined;
|
|
503
|
+
const admitted = fronds.admits(surface, entity);
|
|
504
|
+
if (admitted === false) return undefined;
|
|
505
|
+
|
|
506
|
+
return admitted ? (own ?? effectiveByKey.get(facadeKeyOf(entity))) : own;
|
|
521
507
|
};
|
|
522
508
|
|
|
523
|
-
|
|
524
|
-
const presenterFor = (entity: string): unknown | undefined => {
|
|
525
|
-
const owner = fronds.owner(entity);
|
|
526
|
-
if (!owner) return undefined;
|
|
509
|
+
const presenterFor = (entity: string): unknown | undefined => ownedBy(fronds, container, entity, presenterKeyOf(entity));
|
|
527
510
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
511
|
+
return { resolve, schemaFor, facadeFor, operationsFor, presenterFor };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Registered under the class name, for type-based DI. A logger holds no level — it reads
|
|
516
|
+
* `setLogLevel`'s at each line — and a frond declaring `class X extends Logger` takes the key.
|
|
517
|
+
*/
|
|
518
|
+
function registerBuiltins(container: Container, carry: Carry): void {
|
|
519
|
+
container.registerValue('Logger', new Logger('app', { carry }));
|
|
520
|
+
container.register('Config', Config, { lifetime: 'singleton' });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** `remotes:` IS the topology statement: the frond's code may sit here, it runs elsewhere. */
|
|
524
|
+
function remoteRouterOf(options: CreateAppOptions): RemoteRouter | undefined {
|
|
525
|
+
const declared = Object.entries(options.remotes ?? {});
|
|
526
|
+
if (declared.length === 0) return undefined;
|
|
527
|
+
|
|
528
|
+
if (!options.remoteTransport) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
'createApp: `remotes` is declared but `remoteTransport` is missing — pass one (e.g. from @fougere/transport-http).',
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return createRemoteRouter(Object.fromEntries(declared), options.remoteTransport);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Every check of a phase, said at once — a boot that stops at the first shows the next after a restart. */
|
|
538
|
+
function refuseWhatDoesNotHold(refused: Diagnostic[]): void {
|
|
539
|
+
const refusal = refusalOf(refused, 'declaration(s) that do not hold');
|
|
540
|
+
if (refusal) throw refusal;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* A `ports:` key no port matched reads as a choice that was made. Said once every frond is
|
|
545
|
+
* installed: the entry is app-wide, and no single frond can tell a typo from a neighbour's port.
|
|
546
|
+
*/
|
|
547
|
+
function warnAboutUnboundPorts(options: CreateAppOptions, boundPorts: Set<string>, log: Logger): void {
|
|
548
|
+
const unbound = Object.keys(options.ports ?? {}).filter((port) => !boundPorts.has(port));
|
|
549
|
+
if (unbound.length === 0) return;
|
|
550
|
+
|
|
551
|
+
log.warn(
|
|
552
|
+
`[ports] ${unbound.join(', ')} — named in fougere.config.ts, but no scanned class extends `
|
|
553
|
+
+ 'them, so nothing was chosen. Check the spelling, or drop the entry.',
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Read from who SUBSCRIBED, so a third party's destination is left alone by the two middlewares that observe every operation. */
|
|
558
|
+
function markLineCarriers(emissions: Emissions): void {
|
|
559
|
+
for (const facade of emissions.facadesFor(LOG_LINE)) {
|
|
560
|
+
CARRIES_LINE.add(addressOf(facade));
|
|
561
|
+
CARRIES_LINE.add(facade);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* A handler another process serves: a stand-in for whoever asks for it by type, and a route for
|
|
567
|
+
* whoever calls it. A dependency names the type as written — `ProductHandler` — while a card
|
|
568
|
+
* declares `product`.
|
|
569
|
+
*/
|
|
570
|
+
function answerRemotes(
|
|
571
|
+
{ container, routeRegistry, dispatcher, getMiddlewares }: Pick<Assembly, 'container' | 'routeRegistry' | 'dispatcher' | 'getMiddlewares'>,
|
|
572
|
+
remoteRouter: RemoteRouter,
|
|
573
|
+
): void {
|
|
574
|
+
container.setFallback((name) => (isFacadeKey(name) ? facadeOperations(dispatcher, addressOf(name)) : undefined));
|
|
575
|
+
|
|
576
|
+
const remoteFacades = new Map<string, Record<string, Function>>();
|
|
577
|
+
routeRegistry.addResolver(remoteRoutes((entity) => {
|
|
578
|
+
const known = remoteFacades.get(entity);
|
|
579
|
+
if (known) return known;
|
|
580
|
+
const facade = createRemoteFacade(entity, remoteRouter, getMiddlewares);
|
|
581
|
+
remoteFacades.set(entity, facade);
|
|
582
|
+
|
|
583
|
+
return facade;
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Refused rather than replaced: two declarations of one name would make the answer depend on wiring order. */
|
|
588
|
+
function serveRpcOn(routeRegistry: RouteRegistry): App['serveRpc'] {
|
|
589
|
+
return (op, answer) => {
|
|
590
|
+
const address = new RouteAddress({ entity: 'rpc', operation: op });
|
|
591
|
+
if (routeRegistry.find(address)) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`[claim] rpc operation '${op}' is already served; a second declaration would depend on wiring order.\n`
|
|
594
|
+
+ ' Two extensions declare it — keep one out of `extensions:`.',
|
|
595
|
+
);
|
|
532
596
|
}
|
|
597
|
+
|
|
598
|
+
routeRegistry.register(new OperationRoute('system', address, (call) => answer(call.invocation, call.address.surface)));
|
|
533
599
|
};
|
|
600
|
+
}
|
|
534
601
|
|
|
535
|
-
|
|
602
|
+
/** A frond's extension travels with its code, so it mounts on whichever process serves that frond. */
|
|
603
|
+
function frondExtensions(fronds: Fronds): Extension[] {
|
|
604
|
+
return fronds.flatMap((frond) => (frond.extensions ?? []).map((one) => ({ name: one.name, ...one.extension })));
|
|
536
605
|
}
|
|
537
606
|
|
|
538
607
|
/**
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
608
|
+
* The boot's held lines, and every line after them, handed to the app's destinations — or let go
|
|
609
|
+
* when it has none. After the ascent, because handing them over resolves the destination, and what
|
|
610
|
+
* an extension's destination depends on is registered by that extension's `up`: resolved before,
|
|
611
|
+
* every held line died on `'LogRing' is not registered` (`demos/observability`). A line keeps
|
|
612
|
+
* `at`, when it was WRITTEN, which for a held boot line is not when it is handed over.
|
|
542
613
|
*/
|
|
543
|
-
|
|
544
|
-
if (!
|
|
614
|
+
function announceLines(emissions: Emissions, container: Container, carry: Carry): (() => void) | undefined {
|
|
615
|
+
if (!emissions.listensTo().includes(LOG_LINE)) {
|
|
616
|
+
carry.forget();
|
|
545
617
|
|
|
546
|
-
|
|
547
|
-
throw new Error('createApp: `auth` is set but `storageFactory` is missing — auth providers need it to back their adapter. Pass one through CreateAppOptions.storageFactory.');
|
|
548
|
-
}
|
|
549
|
-
if (options.db === undefined) {
|
|
550
|
-
throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
|
|
618
|
+
return undefined;
|
|
551
619
|
}
|
|
552
620
|
|
|
553
|
-
|
|
554
|
-
const runtime = await options.auth.create({ db: options.db, storageFactory: options.storageFactory });
|
|
555
|
-
log.info(`auth ready — mounted at ${runtime.basePath}`);
|
|
621
|
+
const emit = container.resolve<Emit<LogLine>>(emitKeyOf(LogLine.name));
|
|
556
622
|
|
|
557
|
-
return
|
|
623
|
+
return carry.to(({ at, ...line }: LogRecord) => void emit({ ...line, at: new Date(at) }));
|
|
558
624
|
}
|
|
559
625
|
|
|
560
626
|
/** Bootstrap a fougere application. */
|
|
561
627
|
export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
562
628
|
const container = (options.createContainer ?? createContainer)();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
// releasing has to work from the first line the boot takes something.
|
|
566
|
-
// The conventional ascent, ordered here: tables, then rows, then whatever the host took
|
|
567
|
-
// on. Four hosts assembled these two members themselves — the order is not theirs to
|
|
568
|
-
// choose, and a host that forgot lost its migration in silence.
|
|
569
|
-
const appLifecycle = new AppLifecycle().add(
|
|
570
|
-
migrating(options.migrate),
|
|
571
|
-
seeding(),
|
|
572
|
-
...(options.extensions ?? []),
|
|
573
|
-
);
|
|
574
|
-
/** The app once it exists — a refusal before that releases the two levels that do. */
|
|
575
|
-
let built: App | undefined;
|
|
576
|
-
/** Where THIS boot's lines wait — never a process-wide slot, see `Carry`. */
|
|
629
|
+
// Tables, then rows, then whatever the host took on — the order is not a host's to choose.
|
|
630
|
+
const appLifecycle = new AppLifecycle().add(migrating(options.migrate), seeding(), ...(options.extensions ?? []));
|
|
577
631
|
const carry = new Carry();
|
|
578
|
-
|
|
632
|
+
let built: App | undefined;
|
|
579
633
|
let stopAnnouncing: (() => void) | undefined;
|
|
580
634
|
|
|
581
|
-
/**
|
|
582
|
-
* Everything this app holds, let go in reverse of how it was taken: what an extension took on
|
|
583
|
-
* last, then the container's own, then whoever handed a resource in.
|
|
584
|
-
*/
|
|
635
|
+
/** What an extension took on last, then the container's own, then whoever handed a resource in. */
|
|
585
636
|
const release = async (): Promise<void> => {
|
|
586
|
-
// Every level is told to close even when one refuses, and the refusals leave together —
|
|
587
|
-
// the rule `Lifecycle.down` applies INSIDE its list, applied ACROSS the three. Stated
|
|
588
|
-
// there and broken here, a refusing extension took the container and the connection
|
|
589
|
-
// down with it, which is the leak this gesture exists to prevent.
|
|
590
|
-
const refused: unknown[] = [];
|
|
591
|
-
// Whatever is still held will never reach a destination — the console had it.
|
|
592
637
|
stopAnnouncing?.();
|
|
593
638
|
carry.forget();
|
|
594
|
-
|
|
639
|
+
|
|
640
|
+
await closeAll([
|
|
595
641
|
...(built ? [() => appLifecycle.down(built!)] : []),
|
|
596
642
|
() => container.dispose(),
|
|
597
643
|
() => options.onDispose?.(),
|
|
598
|
-
];
|
|
599
|
-
for (const level of levels) {
|
|
600
|
-
try {
|
|
601
|
-
await level();
|
|
602
|
-
} catch (error) {
|
|
603
|
-
// Flattened one level: an extension's refusals are already an AggregateError, and
|
|
604
|
-
// nesting them would make the caller unwrap twice to read one list.
|
|
605
|
-
if (error instanceof AggregateError) refused.push(...error.errors);
|
|
606
|
-
else refused.push(error);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
if (refused.length > 0) {
|
|
610
|
-
throw new AggregateError(refused, `${refused.length} refusal(s) while releasing the app`);
|
|
611
|
-
}
|
|
644
|
+
], 'refusal(s) while releasing the app');
|
|
612
645
|
};
|
|
613
646
|
|
|
614
647
|
try {
|
|
615
|
-
// Boot chatter is debug by default; a host (e.g. the CLI) can quiet it.
|
|
616
648
|
const log = new Logger('boot:app', { carry });
|
|
617
|
-
|
|
618
|
-
// Builtins — registered under class name (PascalCase) for type-based DI.
|
|
619
|
-
// No level here and none anywhere: a logger consults `setLogLevel`'s value at each
|
|
620
|
-
// emission, so this instance survives a level change and so does every handler that
|
|
621
|
-
// was handed it. A frond declaring `class X extends Logger` takes this key over,
|
|
622
|
-
// like any other port.
|
|
623
|
-
container.registerValue('Logger', new Logger('app', { carry }));
|
|
624
|
-
container.register('Config', Config, { lifetime: 'singleton' });
|
|
649
|
+
registerBuiltins(container, carry);
|
|
625
650
|
log.debug('builtins registered (Logger, Config)');
|
|
626
651
|
|
|
627
652
|
const { fronds, operationModel } = await readFronds(options, log);
|
|
628
|
-
|
|
629
|
-
const authRuntime = await authFor(options, log);
|
|
630
|
-
|
|
631
|
-
// Remote routing — validated at boot: declaring remotes without a transport is a config error.
|
|
632
|
-
// A remote declaration wins over local presence: `remotes: { blog: url }` IS
|
|
633
|
-
// the topology statement — the frond's code may sit in fronds/**, it runs elsewhere.
|
|
634
|
-
const declaredRemotes = Object.entries(options.remotes ?? {});
|
|
635
|
-
if (declaredRemotes.length > 0 && !options.remoteTransport) {
|
|
636
|
-
throw new Error(
|
|
637
|
-
'createApp: `remotes` is declared but `remoteTransport` is missing — pass one (e.g. from @fougere/transport-http).',
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
const remoteRouter = declaredRemotes.length > 0 && options.remoteTransport
|
|
641
|
-
? createRemoteRouter(Object.fromEntries(declaredRemotes), options.remoteTransport)
|
|
642
|
-
: undefined;
|
|
643
|
-
|
|
653
|
+
const remoteRouter = remoteRouterOf(options);
|
|
644
654
|
const { inflight, routeRegistry, dispatchLifecycle, dispatcher, localDispatcher, getMiddlewares, use } =
|
|
645
655
|
dispatching(options, log, fronds, () => journalOf());
|
|
646
656
|
|
|
647
|
-
/** What every check of this boot writes into — refused together, once they have all run. */
|
|
648
657
|
const refused: Diagnostic[] = [];
|
|
649
|
-
const relations: RelationCheck[] = [];
|
|
650
658
|
keyClaims(fronds, options.remotes, refused);
|
|
651
|
-
|
|
652
|
-
// the route registry collides first, and names a route instead of the two fronds.
|
|
653
|
-
const claimed = refusalOf(refused, 'declaration(s) that do not hold');
|
|
654
|
-
if (claimed) throw claimed;
|
|
659
|
+
refuseWhatDoesNotHold(refused);
|
|
655
660
|
|
|
656
661
|
const { entityByName, frondOf, emissions } = whatTheAppKnows(fronds, container, options, refused);
|
|
657
|
-
|
|
658
|
-
/** Canonical operation tables, indexed by the same audience key as their facades. */
|
|
659
662
|
const effectiveByKey = new Map<string, EffectiveOperationsMap>();
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
[...operations].map(([name, operation]) => [name, operation as OperationContract] as const),
|
|
663
|
-
);
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* The storage an entity is backed by — the dual of `facadeFor`, which serves its client-facing
|
|
667
|
-
* facade.
|
|
668
|
-
*/
|
|
669
|
-
const storageFor = (entity: string): unknown | undefined => {
|
|
670
|
-
const owner = fronds.owner(entity);
|
|
671
|
-
if (!owner) return undefined;
|
|
672
|
-
|
|
673
|
-
const key = storageKeyOf(entity);
|
|
674
|
-
try {
|
|
675
|
-
return container.resolve<Container>(`frond:${owner.name}`).resolve(key);
|
|
676
|
-
} catch {
|
|
677
|
-
return undefined;
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
// What carries a release writes none: an instrumentation frond's own rows are kept while a
|
|
682
|
-
// release happens, and journalling them would begin one inside the one being written down.
|
|
663
|
+
const storageFor = <T = Record<string, unknown>>(entity: string): Storage<T> | undefined =>
|
|
664
|
+
ownedBy<Storage<T>>(fronds, container, entity, storageKeyOf(entity));
|
|
683
665
|
const { hosting, journalOf } = hostingFor({
|
|
684
|
-
fronds, container, options,
|
|
666
|
+
fronds, container, options, remoteRouter, entityByName, frondOf, storageOf: storageFor,
|
|
685
667
|
});
|
|
686
668
|
|
|
687
|
-
|
|
688
|
-
// can say so rather than look obeyed.
|
|
669
|
+
const relations: RelationCheck[] = [];
|
|
689
670
|
const boundPorts = new Set<string>();
|
|
690
|
-
|
|
691
|
-
// Register frond scopes
|
|
692
|
-
// What every frond is installed into, and reads while it is: one container, one route
|
|
693
|
-
// table, one emission list — so what a frond serves is there for the next one to find.
|
|
694
671
|
const assembly: Assembly = {
|
|
695
672
|
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
696
673
|
boundPorts, refused, relations, hosting, operationModel, entityByName, frondOf, contractsOf,
|
|
@@ -699,62 +676,16 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
699
676
|
for (const frond of fronds) await installFrond(frond, assembly);
|
|
700
677
|
|
|
701
678
|
warnAboutRelations(relations, hosting, log);
|
|
679
|
+
refused.push(...unpaired(fronds.flatMap((frond) => frond.entities), hosting));
|
|
680
|
+
refuseWhatDoesNotHold(refused);
|
|
681
|
+
warnAboutUnboundPorts(options, boundPorts, log);
|
|
702
682
|
|
|
703
|
-
//
|
|
704
|
-
// next one visible only after a fix and a restart.
|
|
705
|
-
const bootRefusal = refusalOf(refused, 'declaration(s) that do not hold');
|
|
706
|
-
if (bootRefusal) throw bootRefusal;
|
|
707
|
-
|
|
708
|
-
// A `ports:` key that matched no port anywhere reads as a choice that was made, and
|
|
709
|
-
// was not. Said once, at the end, because the entry is app-wide while a port is a
|
|
710
|
-
// frond's — no single frond can tell whether a key is a typo or a neighbour's.
|
|
711
|
-
const unused = Object.keys(options.ports ?? {}).filter((port) => !boundPorts.has(port));
|
|
712
|
-
if (unused.length > 0) {
|
|
713
|
-
log.warn(
|
|
714
|
-
`[ports] ${unused.join(', ')} — named in fougere.config.ts, but no scanned class extends `
|
|
715
|
-
+ 'them, so nothing was chosen. Check the spelling, or drop the entry.',
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Once every facade exists: what is announced here and what is listened to are both known.
|
|
683
|
+
// A pipe order and an `Emit<T, A>` are read here, once every facade exists.
|
|
720
684
|
emissions.register();
|
|
685
|
+
refuseWhatDoesNotHold(refused);
|
|
686
|
+
markLineCarriers(emissions);
|
|
721
687
|
|
|
722
|
-
|
|
723
|
-
// which is where a pipe order and an `Emit<T, A>` are read.
|
|
724
|
-
const announced = refusalOf(refused, 'declaration(s) that do not hold');
|
|
725
|
-
if (announced) throw announced;
|
|
726
|
-
|
|
727
|
-
// Which facades carry a line, read from who SUBSCRIBED — so a third party's destination
|
|
728
|
-
// is left alone by the two middlewares that observe every operation.
|
|
729
|
-
for (const facade of emissions.facadesFor(LOG_LINE)) {
|
|
730
|
-
CARRIES_LINE.add(facade.replace(/Handler$/, '').replace(/^./, (c) => c.toLowerCase()));
|
|
731
|
-
CARRIES_LINE.add(facade);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/** The last resort, held by the container so every resolution path shares it. */
|
|
735
|
-
container.setFallback?.((name) => {
|
|
736
|
-
if (!remoteRouter) return undefined;
|
|
737
|
-
if (!name.endsWith('Handler') || name.includes(':')) return undefined;
|
|
738
|
-
// Façade-shaped stand-in; routing happens lazily at the first call. Through
|
|
739
|
-
// `lowerFirst` because a DEPENDENCY names the type as written — `ProductHandler`,
|
|
740
|
-
// PascalCase — while a card declares `product`, so the raw strip asked the router for
|
|
741
|
-
// 'Product' and every by-type dependency on a remote handler answered NOT_FOUND.
|
|
742
|
-
return facadeOperations(
|
|
743
|
-
dispatcher,
|
|
744
|
-
lowerFirst(name.replace(/Handler$/, '')),
|
|
745
|
-
);
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
if (remoteRouter) {
|
|
749
|
-
const remoteFacades = new Map<string, Record<string, Function>>();
|
|
750
|
-
routeRegistry.addResolver(remoteRoutes((entity) => {
|
|
751
|
-
const known = remoteFacades.get(entity);
|
|
752
|
-
if (known) return known;
|
|
753
|
-
const facade = createRemoteFacade(entity, remoteRouter, getMiddlewares);
|
|
754
|
-
remoteFacades.set(entity, facade);
|
|
755
|
-
return facade;
|
|
756
|
-
}));
|
|
757
|
-
}
|
|
688
|
+
if (remoteRouter) answerRemotes(assembly, remoteRouter);
|
|
758
689
|
|
|
759
690
|
const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
|
|
760
691
|
container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log,
|
|
@@ -763,13 +694,9 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
763
694
|
const app: App = {
|
|
764
695
|
container,
|
|
765
696
|
fronds,
|
|
766
|
-
// What this app publishes, straight from fougere.config.ts — the facades read it,
|
|
767
|
-
// so an undeclared adapter serves nothing whatever a host mounted.
|
|
768
697
|
adapters: options.adapters ?? {},
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
// because a key said so, and the two disagree exactly when something is misconfigured.
|
|
772
|
-
remotes: Object.freeze({ ...(options.remotes ?? {}) }),
|
|
698
|
+
// As DECLARED, beside what the runtime observes — the two disagree exactly when something is misconfigured.
|
|
699
|
+
remotes: Object.freeze({ ...options.remotes }),
|
|
773
700
|
dispatch: (call) => dispatcher.dispatch(call),
|
|
774
701
|
local: localDispatcher,
|
|
775
702
|
resolve,
|
|
@@ -781,87 +708,31 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
781
708
|
storageFor,
|
|
782
709
|
presenterFor,
|
|
783
710
|
dispose: release,
|
|
784
|
-
drain: (timeoutMs?: number) =>
|
|
711
|
+
drain: (timeoutMs?: number) => inflight.drain(timeoutMs),
|
|
785
712
|
inFlight: () => inflight.count,
|
|
786
713
|
[Symbol.asyncDispose]: release,
|
|
787
|
-
serveRpc
|
|
788
|
-
// Refused rather than replaced: two declarations of one name would make the answer
|
|
789
|
-
// depend on wiring order, and `discover` is in here precisely so it cannot be taken.
|
|
790
|
-
const address = new RouteAddress({ entity: 'rpc', operation: op });
|
|
791
|
-
if (routeRegistry.find(address)) {
|
|
792
|
-
throw new Error(
|
|
793
|
-
`[claim] rpc operation '${op}' is already served; a second declaration would depend on wiring order.\n`
|
|
794
|
-
+ ' Two extensions declare it — keep one out of `extensions:`.',
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
routeRegistry.register(new OperationRoute(
|
|
798
|
-
'system',
|
|
799
|
-
address,
|
|
800
|
-
(call) => answer(call.invocation, call.address.surface),
|
|
801
|
-
));
|
|
802
|
-
},
|
|
714
|
+
serveRpc: serveRpcOn(routeRegistry),
|
|
803
715
|
extensions: () => appLifecycle.names(),
|
|
804
|
-
observe(observer)
|
|
805
|
-
return dispatchLifecycle.add(observer);
|
|
806
|
-
},
|
|
716
|
+
observe: (observer) => dispatchLifecycle.add(observer),
|
|
807
717
|
use(...args: [AppMiddleware] | [string, AppMiddleware]): void {
|
|
808
718
|
return typeof args[0] === 'string'
|
|
809
719
|
? use(args[1] as AppMiddleware, args[0])
|
|
810
720
|
: use(args[0] as AppMiddleware);
|
|
811
721
|
},
|
|
812
|
-
auth: authRuntime,
|
|
813
722
|
};
|
|
814
723
|
|
|
815
|
-
// The card is an rpc op like any other, so one registry answers and one refusal names
|
|
816
|
-
// what is served. A package's op is declared the same way, from outside.
|
|
817
|
-
// What a release is, resolved once and registered: a package that drives one asks for it
|
|
818
|
-
// by type, the way every other dependency is asked for.
|
|
819
724
|
container.registerValue('Releasing', releasing(hosting));
|
|
820
|
-
|
|
821
725
|
serveCoreRpc(app, hosting, storageFor);
|
|
822
726
|
|
|
823
|
-
// The last thing the boot does, and the first thing a release undoes. An extension may
|
|
824
|
-
// await here — which is what a provider needing to OPEN something could never do.
|
|
825
727
|
built = app;
|
|
826
|
-
|
|
827
|
-
// What the FRONDS declared, folded in after what the host handed over — a frond's
|
|
828
|
-
// extension is written beside the code it instruments and travels with it, so it mounts
|
|
829
|
-
// on whichever process ends up serving that frond. Added here rather than at the top
|
|
830
|
-
// because the fronds are read below that line, and `up` has not run yet.
|
|
831
|
-
appLifecycle.add(...fronds.flatMap((frond) => (frond.extensions ?? []).map((one) => ({
|
|
832
|
-
name: one.name,
|
|
833
|
-
...one.extension,
|
|
834
|
-
}))));
|
|
835
|
-
|
|
728
|
+
appLifecycle.add(...frondExtensions(fronds));
|
|
836
729
|
await appLifecycle.up(app);
|
|
837
|
-
|
|
838
|
-
// The boot's own lines, and every line after them. Held until here because a boot
|
|
839
|
-
// writes most of what a process logs and writes it before any facade exists — so the
|
|
840
|
-
// lines that say what this app is made of are the ones a destination would miss.
|
|
841
|
-
// `LogLine` is core's for this reason: naming it costs no optional package.
|
|
842
|
-
//
|
|
843
|
-
// AFTER the ascent, because handing them over RESOLVES the destination, and what a
|
|
844
|
-
// destination an extension brought depends on is registered by that same extension's
|
|
845
|
-
// `up`: `calls()` registers `LogRing` and `ErrorRing` there, and `KeepHandler` asks for
|
|
846
|
-
// both. Resolved before the ascent, every held line died on `'LogRing' is not
|
|
847
|
-
// registered` — measured on `demos/observability`.
|
|
848
|
-
if (emissions.listensTo().includes(LOG_LINE)) {
|
|
849
|
-
const emit = container.resolve<Emit<LogLine>>(emitKeyOf(LogLine.name));
|
|
850
|
-
// `at` is the record's own epoch, and the entity says `created()` — so the line
|
|
851
|
-
// keeps WHEN IT WAS WRITTEN rather than when it was handed over, which for a held
|
|
852
|
-
// boot line is a different moment.
|
|
853
|
-
stopAnnouncing = carry.to(({ at, ...line }: LogRecord) => void emit({ ...line, at: new Date(at) }));
|
|
854
|
-
} else {
|
|
855
|
-
// No destination in this app: the console had them, and holding more would grow.
|
|
856
|
-
carry.forget();
|
|
857
|
-
}
|
|
730
|
+
stopAnnouncing = announceLines(emissions, container, carry);
|
|
858
731
|
|
|
859
732
|
return app;
|
|
860
733
|
} catch (cause) {
|
|
861
|
-
// The caller
|
|
862
|
-
//
|
|
863
|
-
// connection to give us would otherwise leak it on every refusal — a source, a scan
|
|
864
|
-
// that threw, a port bound twice, and not only an extension that refused to rise.
|
|
734
|
+
// The caller never receives the app that would carry `onDispose` back, so a refused boot
|
|
735
|
+
// releases what it took itself — a source, a scan that threw, a port bound twice.
|
|
865
736
|
try {
|
|
866
737
|
await release();
|
|
867
738
|
} catch (refused) {
|
|
@@ -872,4 +743,8 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
872
743
|
}
|
|
873
744
|
throw cause;
|
|
874
745
|
}
|
|
875
|
-
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function contractsOf(operations: EffectiveOperationsMap): OperationsMap {
|
|
749
|
+
return new Map([...operations].map(([name, operation]) => [name, operation as OperationContract] as const));
|
|
750
|
+
}
|