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