@fougere/core 0.11.0-alpha.0 → 0.12.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/FougereConfig.d.ts +10 -6
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +19 -4
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts +0 -6
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts +2 -9
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +0 -1
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/FrondsStated.d.ts +54 -0
- package/dist/FrondsStated.d.ts.map +1 -0
- package/dist/FrondsStated.js +26 -0
- package/dist/FrondsStated.js.map +1 -0
- package/dist/StatedFrond.d.ts +21 -0
- package/dist/StatedFrond.d.ts.map +1 -0
- package/dist/StatedFrond.js +34 -0
- package/dist/StatedFrond.js.map +1 -0
- package/dist/StatedModules.d.ts +19 -0
- package/dist/StatedModules.d.ts.map +1 -0
- package/dist/StatedModules.js +37 -0
- package/dist/StatedModules.js.map +1 -0
- package/dist/boot/CreateAppOptions.d.ts +19 -2
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts +9 -0
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +11 -5
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +342 -307
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/declared.d.ts.map +1 -1
- package/dist/boot/declared.js +3 -0
- package/dist/boot/declared.js.map +1 -1
- package/dist/boot/frame.d.ts +0 -4
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js +31 -14
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +4 -3
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/install.d.ts +9 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +271 -185
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/nesting.d.ts +33 -0
- package/dist/boot/nesting.d.ts.map +1 -0
- package/dist/boot/nesting.js +164 -0
- package/dist/boot/nesting.js.map +1 -0
- package/dist/boot/ownership.d.ts +0 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +66 -51
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/ports.js +30 -25
- package/dist/boot/ports.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +31 -28
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +27 -20
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +71 -56
- package/dist/boot/together.js.map +1 -1
- package/dist/descriptor/FrondDescriptor.d.ts +7 -0
- package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
- package/dist/descriptor/MiddlewareEntry.d.ts +6 -5
- package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
- package/dist/descriptor/ProviderEntry.d.ts +7 -0
- package/dist/descriptor/ProviderEntry.d.ts.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 +23 -20
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/PresenterExecutor.d.ts +6 -0
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
- package/dist/dispatch/PresenterExecutor.js +30 -22
- package/dist/dispatch/PresenterExecutor.js.map +1 -1
- package/dist/dispatch/Release.js +53 -47
- package/dist/dispatch/Release.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts +21 -0
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +84 -66
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -1
- package/dist/node.js.map +1 -1
- package/dist/storage/Storage.js +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +29 -21
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +14 -0
- package/dist/verify.js.map +1 -1
- package/dist/wire/Invocation.d.ts +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js +29 -57
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js +22 -19
- package/dist/wire/drift.js.map +1 -1
- package/package.json +4 -4
- package/src/FougereConfig.ts +24 -10
- package/src/FrondConfig.ts +0 -6
- package/src/FrondDeclaration.ts +2 -7
- package/src/FrondsStated.ts +72 -0
- package/src/StatedFrond.ts +51 -0
- package/src/StatedModules.ts +58 -0
- package/src/boot/CreateAppOptions.ts +19 -2
- package/src/boot/Emissions.ts +16 -5
- package/src/boot/bootstrap.ts +478 -359
- package/src/boot/declared.ts +3 -0
- package/src/boot/frame.ts +42 -11
- package/src/boot/hosted.ts +4 -3
- package/src/boot/install.ts +388 -212
- package/src/boot/nesting.ts +196 -0
- package/src/boot/ownership.ts +79 -51
- package/src/boot/ports.ts +45 -31
- package/src/boot/remote.ts +47 -29
- package/src/boot/seed.ts +36 -27
- package/src/boot/together.ts +98 -63
- package/src/descriptor/FrondDescriptor.ts +7 -0
- package/src/descriptor/MiddlewareEntry.ts +6 -5
- package/src/descriptor/ProviderEntry.ts +7 -0
- package/src/dispatch/HandlerFacade.ts +27 -17
- package/src/dispatch/PresenterExecutor.ts +33 -22
- package/src/dispatch/Release.ts +68 -44
- package/src/dispatch/StorageGuard.ts +90 -69
- package/src/index.ts +3 -0
- package/src/node.ts +2 -1
- package/src/storage/Storage.ts +2 -2
- package/src/storage/Store.ts +39 -18
- package/src/verify.ts +15 -0
- package/src/wire/Invocation.ts +2 -1
- package/src/wire/binding.ts +26 -58
- package/src/wire/drift.ts +48 -21
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who inherits code from whom, read off `FougereConfig.fronds` and judged before anything is
|
|
3
|
+
* built. It reads no disk and holds no container: the scopes it decides are built later, in
|
|
4
|
+
* `install.ts`, and everything here is a fact about names.
|
|
5
|
+
*
|
|
6
|
+
* Documented: [fronds](https://fougere.dev/docs/infra/fronds).
|
|
7
|
+
*/
|
|
8
|
+
import type { Diagnostic } from '../diagnostic.js';
|
|
9
|
+
import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
|
|
10
|
+
import { Fronds } from '../descriptor/Fronds.js';
|
|
11
|
+
import { statesModule, type FrondsStated } from '../FrondsStated.js';
|
|
12
|
+
import { statedFronds } from '../StatedFrond.js';
|
|
13
|
+
|
|
14
|
+
/** A frond that holds what a family shares serves nothing — which is what keeps it here. */
|
|
15
|
+
function refuseServingParent(
|
|
16
|
+
parent: FrondDescriptor,
|
|
17
|
+
children: string[],
|
|
18
|
+
refused: Diagnostic[],
|
|
19
|
+
): boolean {
|
|
20
|
+
if (parent.handlers.length === 0) return false;
|
|
21
|
+
|
|
22
|
+
const served = [...new Set(parent.handlers.map((handler) => handler.address))];
|
|
23
|
+
refused.push({
|
|
24
|
+
severity: 'blocking',
|
|
25
|
+
code: 'frond-parent-serves',
|
|
26
|
+
filePath: parent.handlers[0]!.filePath,
|
|
27
|
+
frond: parent.name,
|
|
28
|
+
subject: parent.name,
|
|
29
|
+
message: `'${parent.name}' has children (${children.join(', ')}) and answers at `
|
|
30
|
+
+ `${served.join(', ')}. A frond its family inherits from holds the code they share and `
|
|
31
|
+
+ 'nothing else — with no façade, nothing can call it and nothing can place it elsewhere. '
|
|
32
|
+
+ 'Move its handlers into a child, or take the children out.',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Rows belong to a frond something can call — the family case is not traced, so it is refused. */
|
|
39
|
+
function refuseParentEntities(
|
|
40
|
+
parent: FrondDescriptor,
|
|
41
|
+
children: string[],
|
|
42
|
+
refused: Diagnostic[],
|
|
43
|
+
): void {
|
|
44
|
+
if (parent.entities.length === 0) return;
|
|
45
|
+
|
|
46
|
+
refused.push({
|
|
47
|
+
severity: 'blocking',
|
|
48
|
+
code: 'frond-parent-entities',
|
|
49
|
+
filePath: parent.entities[0]!.filePath,
|
|
50
|
+
frond: parent.name,
|
|
51
|
+
subject: parent.name,
|
|
52
|
+
message: `'${parent.name}' has children (${children.join(', ')}) and declares `
|
|
53
|
+
+ `${parent.entities.map((entity) => entity.name).join(', ')}. A frond they inherit from `
|
|
54
|
+
+ 'holds code, not rows. Declare the entity in a child and import it from there — '
|
|
55
|
+
+ `\`import X from '@fronds/<child>/entities/X.js'\` reaches it from anywhere.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What the tree says, against what this process actually holds. */
|
|
60
|
+
export function nestingOf(
|
|
61
|
+
stated: FrondsStated | undefined,
|
|
62
|
+
fronds: Fronds,
|
|
63
|
+
remotes: Record<string, string> | undefined,
|
|
64
|
+
/** This process carries a subset of the project — `only:`, so an absent name is expected. */
|
|
65
|
+
narrowed = false,
|
|
66
|
+
): { under: Map<string, string>; refused: Diagnostic[] } {
|
|
67
|
+
const refused: Diagnostic[] = [];
|
|
68
|
+
const under = new Map<string, string>();
|
|
69
|
+
if (stated === undefined) return { under, refused };
|
|
70
|
+
|
|
71
|
+
const statements = statedFronds(stated);
|
|
72
|
+
const here = new Map(fronds.map((frond) => [frond.name, frond]));
|
|
73
|
+
const childrenOf = new Map<string, string[]>();
|
|
74
|
+
const inherits = new Set<string>();
|
|
75
|
+
for (const statement of statements) {
|
|
76
|
+
if (statement.extends === undefined) continue;
|
|
77
|
+
|
|
78
|
+
inherits.add(statement.key);
|
|
79
|
+
childrenOf.set(statement.extends, [...(childrenOf.get(statement.extends) ?? []), statement.key]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const statement of statements) {
|
|
83
|
+
if (statement.extends === undefined || !inherits.has(statement.extends)) continue;
|
|
84
|
+
|
|
85
|
+
refused.push({
|
|
86
|
+
severity: 'blocking',
|
|
87
|
+
code: 'frond-extends-chain',
|
|
88
|
+
filePath: 'fougere.config.ts',
|
|
89
|
+
subject: statement.path,
|
|
90
|
+
message: `'${statement.key}' inherits from '${statement.extends}', which inherits from `
|
|
91
|
+
+ `'${statedFronds(stated).find((one) => one.key === statement.extends)?.extends}'. `
|
|
92
|
+
+ 'Inheriting goes one level: a scope hangs off the one above it, and a chain would make '
|
|
93
|
+
+ 'a frond depend on code its own family never named. Move what they share into one '
|
|
94
|
+
+ 'frond they both inherit from.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A frond a family inherits from needs no entry of its own — `extends` is said by the
|
|
99
|
+
// inheritor — so what it may not be is asked of the NAMES, not of the entries.
|
|
100
|
+
for (const [parent, children] of childrenOf) {
|
|
101
|
+
const frond = here.get(parent);
|
|
102
|
+
if (frond === undefined) continue;
|
|
103
|
+
|
|
104
|
+
const placed = stated[parent];
|
|
105
|
+
const address = typeof placed === 'string' ? placed : placed?.remote;
|
|
106
|
+
if (address !== undefined || (remotes && parent in remotes)) {
|
|
107
|
+
refused.push({
|
|
108
|
+
severity: 'blocking',
|
|
109
|
+
code: 'frond-parent-remote',
|
|
110
|
+
filePath: frond.source.path,
|
|
111
|
+
frond: frond.name,
|
|
112
|
+
subject: parent,
|
|
113
|
+
message: `'${parent}' is inherited from by ${children.join(', ')} and is placed at `
|
|
114
|
+
+ `${address ?? remotes?.[parent]}. A frond they inherit from stands in every process `
|
|
115
|
+
+ 'that holds one of them, so it has no address of its own. Give the address to one of '
|
|
116
|
+
+ 'them, or have them inherit from somewhere else.',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!refuseServingParent(frond, children, refused)) refuseParentEntities(frond, children, refused);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const statement of statements) {
|
|
124
|
+
const frond = here.get(statement.key);
|
|
125
|
+
|
|
126
|
+
if (frond === undefined) {
|
|
127
|
+
// Three ways a stated name is absent from this process and says nothing wrong: a module
|
|
128
|
+
// key, which the host imports; an address, whose frond answers elsewhere by definition;
|
|
129
|
+
// and a frond `only:` left out, since the config describes the PROJECT and the flag one
|
|
130
|
+
// process. What is left is a name nothing carries — a typo, and the config is where it
|
|
131
|
+
// was made, so that is where the message points.
|
|
132
|
+
const elsewhere = statement.value !== undefined || (remotes && statement.key in remotes);
|
|
133
|
+
if (!statesModule(statement.key) && !elsewhere && here.size > 0 && !narrowed) {
|
|
134
|
+
refused.push({
|
|
135
|
+
severity: 'blocking',
|
|
136
|
+
code: 'frond-unknown',
|
|
137
|
+
filePath: 'fougere.config.ts',
|
|
138
|
+
subject: statement.path,
|
|
139
|
+
message: `'${statement.path}' names no frond this project holds. It has `
|
|
140
|
+
+ `${[...here.keys()].join(', ') || 'none'}. Check the spelling, or drop the entry.`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (statement.extends !== undefined && !here.has(statement.extends)) {
|
|
147
|
+
refused.push({
|
|
148
|
+
severity: 'blocking',
|
|
149
|
+
code: 'frond-family-split',
|
|
150
|
+
filePath: frond.source.path,
|
|
151
|
+
frond: frond.name,
|
|
152
|
+
subject: statement.path,
|
|
153
|
+
message: `'${statement.key}' inherits from '${statement.extends}', which this process does `
|
|
154
|
+
+ 'not carry. A frond cannot leave the code it resolves behind — carry the two '
|
|
155
|
+
+ `together, or take '${statement.key}' out from under '${statement.extends}'.`,
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (statement.extends !== undefined) under.set(statement.key, statement.extends);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { under, refused };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The same fronds, each parent before the ones inheriting from it — the order the boot installs
|
|
168
|
+
* them in, since a scope hangs off the one above it.
|
|
169
|
+
*
|
|
170
|
+
* A partition and not a sort: inheriting goes one level, so a frond that inherits has nothing
|
|
171
|
+
* under it and two passes are enough. Stable, and the same array when nothing inherits — which
|
|
172
|
+
* is what `orderSeeds` reads.
|
|
173
|
+
*/
|
|
174
|
+
export function parentsFirst(fronds: Fronds, under: Map<string, string>): Fronds {
|
|
175
|
+
if (under.size === 0) return fronds;
|
|
176
|
+
|
|
177
|
+
return Fronds.hosting([
|
|
178
|
+
...fronds.filter((frond) => !under.has(frond.name)),
|
|
179
|
+
...fronds.filter((frond) => under.has(frond.name)),
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The same `under` a boot stamps, for a reader that does not boot — `fougere check` and
|
|
185
|
+
* `fougere explain` resolve the model off a scan, and a scan knows nothing of the tree.
|
|
186
|
+
* Without it they report every inherited dependency as a crossing.
|
|
187
|
+
*/
|
|
188
|
+
export function nested(fronds: Fronds, stated: FrondsStated | undefined): Fronds {
|
|
189
|
+
const { under } = nestingOf(stated, fronds, undefined, true);
|
|
190
|
+
for (const frond of fronds) {
|
|
191
|
+
const parent = under.get(frond.name);
|
|
192
|
+
if (parent !== undefined) frond.extends = parent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return parentsFirst(fronds, under);
|
|
196
|
+
}
|
package/src/boot/ownership.ts
CHANGED
|
@@ -84,6 +84,83 @@ function builtOn(ctor: unknown): string[] {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** `Storage<E>` is not a word of the user's vocabulary — `<E>Repository` is the one way in. */
|
|
87
|
+
/**
|
|
88
|
+
* A provider named `<Entity>Storage` takes the key of that entity's own storage — and the storage
|
|
89
|
+
* is registered second, so the provider is never resolved. The name is already wrong by two other
|
|
90
|
+
* rules, which is why nothing hits it; it is said rather than left silent.
|
|
91
|
+
*/
|
|
92
|
+
function refuseStorageKeyAsProvider(
|
|
93
|
+
provider: ProviderEntry,
|
|
94
|
+
frond: string,
|
|
95
|
+
known: (entity: string) => boolean,
|
|
96
|
+
refused: Diagnostic[],
|
|
97
|
+
): void {
|
|
98
|
+
// The key the boot registers it under, which a bundler may have renamed — the same reading
|
|
99
|
+
// `ProviderEntry.name` exists for.
|
|
100
|
+
const registered = nameOf(provider);
|
|
101
|
+
const held = entityOfStorageKey(registered, known);
|
|
102
|
+
if (!held) return;
|
|
103
|
+
|
|
104
|
+
refused.push({
|
|
105
|
+
severity: 'blocking',
|
|
106
|
+
code: 'storage-key-is-provider',
|
|
107
|
+
filePath: provider.filePath,
|
|
108
|
+
frond,
|
|
109
|
+
subject: registered,
|
|
110
|
+
message: `${registered} is a provider, and it is the container key of ${held}'s own storage `
|
|
111
|
+
+ '— the storage is registered second, so the provider is never resolved. Name it for '
|
|
112
|
+
+ 'what it holds; `<Entity>Storage` belongs to the entity.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Storage is reached through a repository, and an OWNED entity's not even that way: an aggregate's
|
|
118
|
+
* members are reached through it and nowhere else. The ownership question is asked first, so a
|
|
119
|
+
* member says who owns it rather than being told to write a repository it may not have.
|
|
120
|
+
*/
|
|
121
|
+
function refuseStorageReached(
|
|
122
|
+
decl: { ctor: { name: string }; deps: string[]; filePath: string; kind: string },
|
|
123
|
+
frond: string,
|
|
124
|
+
owners: Map<string, string>,
|
|
125
|
+
known: (entity: string) => boolean,
|
|
126
|
+
refused: Diagnostic[],
|
|
127
|
+
): void {
|
|
128
|
+
const allowed = decl.kind === 'provider' ? builtOn(decl.ctor) : [];
|
|
129
|
+
|
|
130
|
+
for (const dep of decl.deps) {
|
|
131
|
+
const entity = entityOfStorageKey(dep, known);
|
|
132
|
+
if (!entity) continue;
|
|
133
|
+
|
|
134
|
+
const owner = owners.get(entity);
|
|
135
|
+
if (owner && owner !== decl.ctor.name) {
|
|
136
|
+
refused.push({
|
|
137
|
+
severity: 'blocking',
|
|
138
|
+
code: 'aggregate-storage-reached',
|
|
139
|
+
filePath: decl.filePath,
|
|
140
|
+
frond,
|
|
141
|
+
subject: `${decl.ctor.name}(${dep})`,
|
|
142
|
+
message: `${decl.ctor.name} asks for ${dep}, and ${owner} owns ${entity}. Nothing else `
|
|
143
|
+
+ `reaches an owned entity's storage — name the operation on ${owner} and ask for it: `
|
|
144
|
+
+ `constructor(private ${entity}: ${owner}) {}`,
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (allowed.includes(entity)) continue;
|
|
149
|
+
|
|
150
|
+
refused.push({
|
|
151
|
+
severity: 'blocking',
|
|
152
|
+
code: 'storage-outside-repository',
|
|
153
|
+
filePath: decl.filePath,
|
|
154
|
+
frond,
|
|
155
|
+
subject: `${decl.ctor.name}(${dep})`,
|
|
156
|
+
message: `${decl.ctor.name} asks for ${dep}. Storage is reached through a repository, never `
|
|
157
|
+
+ `through the port: constructor(private ${entity}: ${repositoryKeyOf(entity)}) {}. It `
|
|
158
|
+
+ `answers every gesture ${dep} does, whether or not anyone wrote the file — and the day `
|
|
159
|
+
+ `${entity} belongs to an aggregate, this line does not move.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
87
164
|
export function storageInUserCode(
|
|
88
165
|
frond: FrondDescriptor,
|
|
89
166
|
owners: Map<string, string>,
|
|
@@ -97,59 +174,10 @@ export function storageInUserCode(
|
|
|
97
174
|
];
|
|
98
175
|
const holders = frond.providers.map((p) => ({ ...p, kind: 'provider' as const }));
|
|
99
176
|
|
|
100
|
-
for (const provider of holders)
|
|
101
|
-
// The key the boot registers it under, which a bundler may have renamed — the same
|
|
102
|
-
// reading `ProviderEntry.name` exists for.
|
|
103
|
-
const registered = nameOf(provider);
|
|
104
|
-
const held = entityOfStorageKey(registered, known);
|
|
105
|
-
if (!held) continue;
|
|
106
|
-
|
|
107
|
-
refused.push({
|
|
108
|
-
severity: 'blocking',
|
|
109
|
-
code: 'storage-key-is-provider',
|
|
110
|
-
filePath: provider.filePath,
|
|
111
|
-
frond: frond.name,
|
|
112
|
-
subject: registered,
|
|
113
|
-
message: `${registered} is a provider, and it is the container key of ${held}'s own storage `
|
|
114
|
-
+ '— the storage is registered second, so the provider is never resolved. Name it for '
|
|
115
|
-
+ 'what it holds; `<Entity>Storage` belongs to the entity.',
|
|
116
|
-
});
|
|
117
|
-
}
|
|
177
|
+
for (const provider of holders) refuseStorageKeyAsProvider(provider, frond.name, known, refused);
|
|
118
178
|
|
|
119
179
|
for (const decl of [...facades, ...holders]) {
|
|
120
|
-
|
|
121
|
-
for (const dep of decl.deps) {
|
|
122
|
-
const entity = entityOfStorageKey(dep, known);
|
|
123
|
-
if (!entity) continue;
|
|
124
|
-
|
|
125
|
-
const owner = owners.get(entity);
|
|
126
|
-
if (owner && owner !== decl.ctor.name) {
|
|
127
|
-
refused.push({
|
|
128
|
-
severity: 'blocking',
|
|
129
|
-
code: 'aggregate-storage-reached',
|
|
130
|
-
filePath: decl.filePath,
|
|
131
|
-
frond: frond.name,
|
|
132
|
-
subject: `${decl.ctor.name}(${dep})`,
|
|
133
|
-
message: `${decl.ctor.name} asks for ${dep}, and ${owner} owns ${entity}. Nothing else `
|
|
134
|
-
+ `reaches an owned entity's storage — name the operation on ${owner} and ask for it: `
|
|
135
|
-
+ `constructor(private ${entity}: ${owner}) {}`,
|
|
136
|
-
});
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (allowed.includes(entity)) continue;
|
|
140
|
-
|
|
141
|
-
refused.push({
|
|
142
|
-
severity: 'blocking',
|
|
143
|
-
code: 'storage-outside-repository',
|
|
144
|
-
filePath: decl.filePath,
|
|
145
|
-
frond: frond.name,
|
|
146
|
-
subject: `${decl.ctor.name}(${dep})`,
|
|
147
|
-
message: `${decl.ctor.name} asks for ${dep}. Storage is reached through a repository, never `
|
|
148
|
-
+ `through the port: constructor(private ${entity}: ${repositoryKeyOf(entity)}) {}. It `
|
|
149
|
-
+ `answers every gesture ${dep} does, whether or not anyone wrote the file — and the day `
|
|
150
|
-
+ `${entity} belongs to an aggregate, this line does not move.`,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
180
|
+
refuseStorageReached(decl, frond.name, owners, known, refused);
|
|
153
181
|
}
|
|
154
182
|
}
|
|
155
183
|
|
package/src/boot/ports.ts
CHANGED
|
@@ -111,35 +111,54 @@ function chain(
|
|
|
111
111
|
stated: string | readonly string[] | undefined,
|
|
112
112
|
refused: Diagnostic[],
|
|
113
113
|
): ProviderEntry[] {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
114
|
+
if (stated !== undefined) return statedChain(port, [...wrappers, ...impls], stated, refused);
|
|
115
|
+
|
|
116
|
+
refuseUndecided(port, impls, wrappers, refused);
|
|
117
|
+
|
|
118
|
+
return [...wrappers, ...impls];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The whole chain, outside in — a string is a chain of one, and the last name is what actually
|
|
123
|
+
* answers. What a deployment wraps its realization with is the same kind of decision as which
|
|
124
|
+
* realization it uses, so it is the same key.
|
|
125
|
+
*/
|
|
126
|
+
function statedChain(
|
|
127
|
+
port: string,
|
|
128
|
+
all: ProviderEntry[],
|
|
129
|
+
stated: string | readonly string[],
|
|
130
|
+
refused: Diagnostic[],
|
|
131
|
+
): ProviderEntry[] {
|
|
132
|
+
const order = typeof stated === 'string' ? [stated] : stated;
|
|
133
|
+
|
|
134
|
+
return order.flatMap((name) => {
|
|
135
|
+
const pick = all.find((one) => nameOf(one) === name);
|
|
136
|
+
if (pick) return [pick];
|
|
137
|
+
|
|
138
|
+
refused.push({
|
|
139
|
+
severity: 'blocking',
|
|
140
|
+
code: 'port-not-extended',
|
|
141
|
+
filePath: all[0]?.filePath ?? 'fougere.config.ts',
|
|
142
|
+
subject: `ports: { ${port}: '${name}' }`,
|
|
143
|
+
message: `${port}: '${name}' does not extend it. What does: ${all.map(nameOf).join(', ')}.`,
|
|
136
144
|
});
|
|
137
|
-
}
|
|
138
145
|
|
|
146
|
+
return [];
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Two realizations, or two wrappers, and nothing saying which — refused rather than settled by
|
|
152
|
+
* scan order, the same reason `remotes` refuses two owners of one entity: whichever won would
|
|
153
|
+
* depend on the order files were read, and the handler would charge the wrong provider silently.
|
|
154
|
+
*/
|
|
155
|
+
function refuseUndecided(
|
|
156
|
+
port: string,
|
|
157
|
+
impls: ProviderEntry[],
|
|
158
|
+
wrappers: ProviderEntry[],
|
|
159
|
+
refused: Diagnostic[],
|
|
160
|
+
): void {
|
|
139
161
|
if (impls.length > 1) {
|
|
140
|
-
// Refusing rather than keeping one, for the reason `remotes` refuses two owners
|
|
141
|
-
// of an entity: whichever won would depend on scan order, and the handler would
|
|
142
|
-
// charge the wrong provider without a word.
|
|
143
162
|
refused.push({
|
|
144
163
|
severity: 'blocking',
|
|
145
164
|
code: 'port-implemented-twice',
|
|
@@ -149,12 +168,9 @@ function chain(
|
|
|
149
168
|
+ 'answers it. Which realization a deployment uses is not a fact about the code — state it: '
|
|
150
169
|
+ `ports: { ${port}: '${nameOf(impls[0]!)}' } in fougere.config.ts.`,
|
|
151
170
|
});
|
|
152
|
-
|
|
153
|
-
return [...wrappers, ...impls];
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
if (wrappers.length > 1) {
|
|
157
|
-
// Same refusal one layer out: two wrappers are an ORDER, and scan order is not one.
|
|
158
174
|
refused.push({
|
|
159
175
|
severity: 'blocking',
|
|
160
176
|
code: 'port-wrapped-twice',
|
|
@@ -166,6 +182,4 @@ function chain(
|
|
|
166
182
|
+ 'in fougere.config.ts.',
|
|
167
183
|
});
|
|
168
184
|
}
|
|
169
|
-
|
|
170
|
-
return [...wrappers, ...impls];
|
|
171
185
|
}
|
package/src/boot/remote.ts
CHANGED
|
@@ -65,36 +65,9 @@ export function createRemoteRouter(
|
|
|
65
65
|
|
|
66
66
|
for (const answered of cards) {
|
|
67
67
|
if (!answered) continue;
|
|
68
|
-
const { label, url, transport, answer } = answered;
|
|
69
|
-
pending.delete(label);
|
|
70
|
-
const card = assertIdentityCard(answer, `Remote '${label}' (${url})`);
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// adding one here would answer a call with a transport to a facade that
|
|
75
|
-
// does not exist.
|
|
76
|
-
for (const facade of frond.facades) {
|
|
77
|
-
const first = claimedBy.get(facade.name);
|
|
78
|
-
/** Two remotes claiming one name is refused, not silently arbitrated. */
|
|
79
|
-
if (first !== undefined && first !== label) {
|
|
80
|
-
throw new FougereError({
|
|
81
|
-
code: ErrorCode.INTERNAL_ERROR,
|
|
82
|
-
message:
|
|
83
|
-
`[claim] Two remotes serve '${facade.name}': '${first}' and '${label}'.\n`
|
|
84
|
-
+ ` A call names an entity, not a frond, so nothing could choose between them.\n`
|
|
85
|
-
+ ` - Keep one of the two out of \`remotes:\`, or\n`
|
|
86
|
-
+ ` - expose one of them under a different entity name.`,
|
|
87
|
-
entity: facade.name,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
claimedBy.set(facade.name, label);
|
|
91
|
-
byEntity.set(facade.name, {
|
|
92
|
-
frond: frond.name,
|
|
93
|
-
transport,
|
|
94
|
-
...(facade.schema ? { schema: Card.fromDescriptor(facade.schema as SchemaDescriptor).toSchema() } : {}),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
69
|
+
pending.delete(answered.label);
|
|
70
|
+
claimFacades(answered, byEntity, claimedBy);
|
|
98
71
|
}
|
|
99
72
|
};
|
|
100
73
|
|
|
@@ -141,3 +114,48 @@ export function createRemoteFacade(
|
|
|
141
114
|
|
|
142
115
|
return dynamicOperations(opFn) as Facade;
|
|
143
116
|
}
|
|
117
|
+
|
|
118
|
+
/** What one remote answered `discover` with, once it has been reached. */
|
|
119
|
+
interface Answered {
|
|
120
|
+
label: string;
|
|
121
|
+
url: string;
|
|
122
|
+
transport: Transport;
|
|
123
|
+
answer: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Which remote serves which facade. Facades only: a fact is not routable — nobody calls it, it
|
|
128
|
+
* arrives — so adding one here would answer a call with a transport to a facade that does not
|
|
129
|
+
* exist. Two remotes claiming one name is refused, never silently arbitrated.
|
|
130
|
+
*/
|
|
131
|
+
function claimFacades(
|
|
132
|
+
{ label, url, transport, answer }: Answered,
|
|
133
|
+
byEntity: Map<string, Route>,
|
|
134
|
+
claimedBy: Map<string, string>,
|
|
135
|
+
): void {
|
|
136
|
+
const card = assertIdentityCard(answer, `Remote '${label}' (${url})`);
|
|
137
|
+
|
|
138
|
+
for (const frond of card.fronds) {
|
|
139
|
+
for (const facade of frond.facades) {
|
|
140
|
+
const first = claimedBy.get(facade.name);
|
|
141
|
+
if (first !== undefined && first !== label) {
|
|
142
|
+
throw new FougereError({
|
|
143
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
144
|
+
message:
|
|
145
|
+
`[claim] Two remotes serve '${facade.name}': '${first}' and '${label}'.\n`
|
|
146
|
+
+ ` A call names an entity, not a frond, so nothing could choose between them.\n`
|
|
147
|
+
+ ` - Keep one of the two out of \`remotes:\`, or\n`
|
|
148
|
+
+ ` - expose one of them under a different entity name.`,
|
|
149
|
+
entity: facade.name,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
claimedBy.set(facade.name, label);
|
|
154
|
+
byEntity.set(facade.name, {
|
|
155
|
+
frond: frond.name,
|
|
156
|
+
transport,
|
|
157
|
+
...(facade.schema ? { schema: Card.fromDescriptor(facade.schema as SchemaDescriptor).toSchema() } : {}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/boot/seed.ts
CHANGED
|
@@ -15,6 +15,33 @@ export interface SeedOrder {
|
|
|
15
15
|
|
|
16
16
|
/** Seeds in dependency order — a `ref()` target is planted before its referrer. */
|
|
17
17
|
export function orderSeeds(fronds: FrondDescriptor[]): SeedOrder {
|
|
18
|
+
const seeds = fronds.flatMap((frond) => frond.seeds);
|
|
19
|
+
const waiting = whatEachWaitsFor(fronds, seeds);
|
|
20
|
+
|
|
21
|
+
const ordered: SeedEntry[] = [];
|
|
22
|
+
const planted = new Set<string>();
|
|
23
|
+
|
|
24
|
+
while (waiting.size > 0) {
|
|
25
|
+
const ready = [...waiting.keys()].find((seed) => [...waiting.get(seed)!].every((dep) => planted.has(dep)));
|
|
26
|
+
if (!ready) break;
|
|
27
|
+
|
|
28
|
+
ordered.push(ready);
|
|
29
|
+
planted.add(ready.entityName.toLowerCase());
|
|
30
|
+
waiting.delete(ready);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { ordered, cycle: [...waiting.keys()] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Only what is actually SEEDED can be waited for: a relation to an entity with no seed is
|
|
38
|
+
* already satisfied by whatever put its rows there, and an entity naming itself waits for
|
|
39
|
+
* nobody.
|
|
40
|
+
*/
|
|
41
|
+
function whatEachWaitsFor(
|
|
42
|
+
fronds: FrondDescriptor[],
|
|
43
|
+
seeds: SeedEntry[],
|
|
44
|
+
): Map<SeedEntry, Set<string>> {
|
|
18
45
|
const refs = new Map<string, Set<string>>();
|
|
19
46
|
for (const frond of fronds) {
|
|
20
47
|
for (const entity of frond.entities) {
|
|
@@ -28,33 +55,13 @@ export function orderSeeds(fronds: FrondDescriptor[]): SeedOrder {
|
|
|
28
55
|
}
|
|
29
56
|
}
|
|
30
57
|
|
|
31
|
-
const seeds = fronds.flatMap((frond) => frond.seeds);
|
|
32
58
|
const seeded = new Set(seeds.map((seed) => seed.entityName.toLowerCase()));
|
|
33
59
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const waiting = new Map(
|
|
37
|
-
seeds.map((seed) => {
|
|
38
|
-
const own = seed.entityName.toLowerCase();
|
|
39
|
-
const targets = [...(refs.get(own) ?? [])].filter((target) => seeded.has(target) && target !== own);
|
|
60
|
+
return new Map(seeds.map((seed) => {
|
|
61
|
+
const own = seed.entityName.toLowerCase();
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const ordered: SeedEntry[] = [];
|
|
46
|
-
const planted = new Set<string>();
|
|
47
|
-
|
|
48
|
-
while (waiting.size > 0) {
|
|
49
|
-
const ready = [...waiting.keys()].find((seed) => [...waiting.get(seed)!].every((dep) => planted.has(dep)));
|
|
50
|
-
if (!ready) break;
|
|
51
|
-
|
|
52
|
-
ordered.push(ready);
|
|
53
|
-
planted.add(ready.entityName.toLowerCase());
|
|
54
|
-
waiting.delete(ready);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { ordered, cycle: [...waiting.keys()] };
|
|
63
|
+
return [seed, new Set([...(refs.get(own) ?? [])].filter((target) => seeded.has(target) && target !== own))] as const;
|
|
64
|
+
}));
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/** Where a seed writes, and what it may skip — resolved per entity. */
|
|
@@ -110,10 +117,12 @@ function facadeFor(app: App, entityName: string): SeedFacade | undefined {
|
|
|
110
117
|
let handler: Record<string, Function> | undefined;
|
|
111
118
|
try { handler = app.resolve<Record<string, Function>>(facadeKeyOf(entityName)); } catch {}
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
const list = handler?.list;
|
|
121
|
+
const create = handler?.create;
|
|
122
|
+
if (typeof list === 'function' && typeof create === 'function') {
|
|
114
123
|
return {
|
|
115
|
-
list: () =>
|
|
116
|
-
write: (item) =>
|
|
124
|
+
list: () => list.call(handler) as Promise<unknown[]>,
|
|
125
|
+
write: (item) => create.call(handler, { params: {}, query: {}, input: item, state: {} }),
|
|
117
126
|
};
|
|
118
127
|
}
|
|
119
128
|
|