@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.
Files changed (146) hide show
  1. package/dist/FougereConfig.d.ts +10 -6
  2. package/dist/FougereConfig.d.ts.map +1 -1
  3. package/dist/FougereConfig.js +19 -4
  4. package/dist/FougereConfig.js.map +1 -1
  5. package/dist/FrondConfig.d.ts +0 -6
  6. package/dist/FrondConfig.d.ts.map +1 -1
  7. package/dist/FrondConfig.js.map +1 -1
  8. package/dist/FrondDeclaration.d.ts +2 -9
  9. package/dist/FrondDeclaration.d.ts.map +1 -1
  10. package/dist/FrondDeclaration.js +0 -1
  11. package/dist/FrondDeclaration.js.map +1 -1
  12. package/dist/FrondsStated.d.ts +54 -0
  13. package/dist/FrondsStated.d.ts.map +1 -0
  14. package/dist/FrondsStated.js +26 -0
  15. package/dist/FrondsStated.js.map +1 -0
  16. package/dist/StatedFrond.d.ts +21 -0
  17. package/dist/StatedFrond.d.ts.map +1 -0
  18. package/dist/StatedFrond.js +34 -0
  19. package/dist/StatedFrond.js.map +1 -0
  20. package/dist/StatedModules.d.ts +19 -0
  21. package/dist/StatedModules.d.ts.map +1 -0
  22. package/dist/StatedModules.js +37 -0
  23. package/dist/StatedModules.js.map +1 -0
  24. package/dist/boot/CreateAppOptions.d.ts +19 -2
  25. package/dist/boot/CreateAppOptions.d.ts.map +1 -1
  26. package/dist/boot/Emissions.d.ts +9 -0
  27. package/dist/boot/Emissions.d.ts.map +1 -1
  28. package/dist/boot/Emissions.js +11 -5
  29. package/dist/boot/Emissions.js.map +1 -1
  30. package/dist/boot/bootstrap.d.ts.map +1 -1
  31. package/dist/boot/bootstrap.js +342 -307
  32. package/dist/boot/bootstrap.js.map +1 -1
  33. package/dist/boot/declared.d.ts.map +1 -1
  34. package/dist/boot/declared.js +3 -0
  35. package/dist/boot/declared.js.map +1 -1
  36. package/dist/boot/frame.d.ts +0 -4
  37. package/dist/boot/frame.d.ts.map +1 -1
  38. package/dist/boot/frame.js +31 -14
  39. package/dist/boot/frame.js.map +1 -1
  40. package/dist/boot/hosted.d.ts.map +1 -1
  41. package/dist/boot/hosted.js +4 -3
  42. package/dist/boot/hosted.js.map +1 -1
  43. package/dist/boot/install.d.ts +9 -0
  44. package/dist/boot/install.d.ts.map +1 -1
  45. package/dist/boot/install.js +271 -185
  46. package/dist/boot/install.js.map +1 -1
  47. package/dist/boot/nesting.d.ts +33 -0
  48. package/dist/boot/nesting.d.ts.map +1 -0
  49. package/dist/boot/nesting.js +164 -0
  50. package/dist/boot/nesting.js.map +1 -0
  51. package/dist/boot/ownership.d.ts +0 -1
  52. package/dist/boot/ownership.d.ts.map +1 -1
  53. package/dist/boot/ownership.js +66 -51
  54. package/dist/boot/ownership.js.map +1 -1
  55. package/dist/boot/ports.js +30 -25
  56. package/dist/boot/ports.js.map +1 -1
  57. package/dist/boot/remote.d.ts.map +1 -1
  58. package/dist/boot/remote.js +31 -28
  59. package/dist/boot/remote.js.map +1 -1
  60. package/dist/boot/seed.d.ts.map +1 -1
  61. package/dist/boot/seed.js +27 -20
  62. package/dist/boot/seed.js.map +1 -1
  63. package/dist/boot/together.d.ts.map +1 -1
  64. package/dist/boot/together.js +71 -56
  65. package/dist/boot/together.js.map +1 -1
  66. package/dist/descriptor/FrondDescriptor.d.ts +7 -0
  67. package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
  68. package/dist/descriptor/MiddlewareEntry.d.ts +6 -5
  69. package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
  70. package/dist/descriptor/ProviderEntry.d.ts +7 -0
  71. package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
  72. package/dist/dispatch/HandlerFacade.d.ts +6 -1
  73. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  74. package/dist/dispatch/HandlerFacade.js +23 -20
  75. package/dist/dispatch/HandlerFacade.js.map +1 -1
  76. package/dist/dispatch/PresenterExecutor.d.ts +6 -0
  77. package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
  78. package/dist/dispatch/PresenterExecutor.js +30 -22
  79. package/dist/dispatch/PresenterExecutor.js.map +1 -1
  80. package/dist/dispatch/Release.js +53 -47
  81. package/dist/dispatch/Release.js.map +1 -1
  82. package/dist/dispatch/StorageGuard.d.ts +21 -0
  83. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  84. package/dist/dispatch/StorageGuard.js +84 -66
  85. package/dist/dispatch/StorageGuard.js.map +1 -1
  86. package/dist/index.d.ts +3 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +3 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/node.d.ts +2 -1
  91. package/dist/node.d.ts.map +1 -1
  92. package/dist/node.js +2 -1
  93. package/dist/node.js.map +1 -1
  94. package/dist/storage/Storage.js +1 -1
  95. package/dist/storage/Storage.js.map +1 -1
  96. package/dist/storage/Store.d.ts.map +1 -1
  97. package/dist/storage/Store.js +29 -21
  98. package/dist/storage/Store.js.map +1 -1
  99. package/dist/verify.d.ts.map +1 -1
  100. package/dist/verify.js +14 -0
  101. package/dist/verify.js.map +1 -1
  102. package/dist/wire/Invocation.d.ts +1 -1
  103. package/dist/wire/Invocation.d.ts.map +1 -1
  104. package/dist/wire/Invocation.js +1 -1
  105. package/dist/wire/Invocation.js.map +1 -1
  106. package/dist/wire/binding.d.ts.map +1 -1
  107. package/dist/wire/binding.js +29 -57
  108. package/dist/wire/binding.js.map +1 -1
  109. package/dist/wire/drift.d.ts.map +1 -1
  110. package/dist/wire/drift.js +22 -19
  111. package/dist/wire/drift.js.map +1 -1
  112. package/package.json +4 -4
  113. package/src/FougereConfig.ts +24 -10
  114. package/src/FrondConfig.ts +0 -6
  115. package/src/FrondDeclaration.ts +2 -7
  116. package/src/FrondsStated.ts +72 -0
  117. package/src/StatedFrond.ts +51 -0
  118. package/src/StatedModules.ts +58 -0
  119. package/src/boot/CreateAppOptions.ts +19 -2
  120. package/src/boot/Emissions.ts +16 -5
  121. package/src/boot/bootstrap.ts +478 -359
  122. package/src/boot/declared.ts +3 -0
  123. package/src/boot/frame.ts +42 -11
  124. package/src/boot/hosted.ts +4 -3
  125. package/src/boot/install.ts +388 -212
  126. package/src/boot/nesting.ts +196 -0
  127. package/src/boot/ownership.ts +79 -51
  128. package/src/boot/ports.ts +45 -31
  129. package/src/boot/remote.ts +47 -29
  130. package/src/boot/seed.ts +36 -27
  131. package/src/boot/together.ts +98 -63
  132. package/src/descriptor/FrondDescriptor.ts +7 -0
  133. package/src/descriptor/MiddlewareEntry.ts +6 -5
  134. package/src/descriptor/ProviderEntry.ts +7 -0
  135. package/src/dispatch/HandlerFacade.ts +27 -17
  136. package/src/dispatch/PresenterExecutor.ts +33 -22
  137. package/src/dispatch/Release.ts +68 -44
  138. package/src/dispatch/StorageGuard.ts +90 -69
  139. package/src/index.ts +3 -0
  140. package/src/node.ts +2 -1
  141. package/src/storage/Storage.ts +2 -2
  142. package/src/storage/Store.ts +39 -18
  143. package/src/verify.ts +15 -0
  144. package/src/wire/Invocation.ts +2 -1
  145. package/src/wire/binding.ts +26 -58
  146. package/src/wire/drift.ts +48 -21
@@ -0,0 +1,72 @@
1
+ import type { NameOf } from './NameOf.js';
2
+
3
+ /**
4
+ * What the config says about the fronds this app is made of — where each one comes from, and
5
+ * which one it inherits code from.
6
+ *
7
+ * `fronds:` on an entry says ONE thing: those fronds resolve what this one declared. It says
8
+ * nothing about placement, and nothing about the right to call — two fronds reach each other
9
+ * through a façade or an announced fact, whatever their place here.
10
+ *
11
+ * The key SUGGESTS what the scan found and refuses nothing: `NameOf<'frond'>` alone would
12
+ * close the record, and a module specifier is a legal key no scan can have seen. `string & {}`
13
+ * is what keeps the union open while an editor still lists the names.
14
+ *
15
+ * Documented: [fronds](https://fougere.dev/docs/infra/fronds).
16
+ */
17
+ export type FrondsStated = {
18
+ readonly [frond in NameOf<'frond'> | (string & {})]?: FrondStated;
19
+ };
20
+
21
+ /**
22
+ * Everything an entry says, or the one string that is the whole of it — an address for a frond
23
+ * that answers elsewhere, the argument for a module key.
24
+ */
25
+ export type FrondStated = string | FrondAttributes;
26
+
27
+ export interface FrondAttributes {
28
+ /**
29
+ * The frond this one inherits code from — its scope hangs off that one's.
30
+ *
31
+ * Said by the INHERITOR, and scalar: a name then lives in one place, so moving a frond from
32
+ * one family to another is one edit and removing it leaves nothing dangling. It is the form
33
+ * `tsconfig`, Maven and Kubernetes all chose for inheritance, and the one that makes
34
+ * "one parent" a fact of the shape rather than a refusal at boot.
35
+ *
36
+ * ONE level: the frond it names may not inherit itself (`frond-extends-chain`). Nothing in
37
+ * this repo has ever wanted a chain, and refusing it is also what keeps a cycle out.
38
+ */
39
+ extends?: NameOf<'frond'> | (string & {});
40
+ /** Where it answers — the same thing the shorthand says. */
41
+ remote?: string;
42
+ /** What the host hands a module key when it imports it. */
43
+ options?: string;
44
+ }
45
+
46
+ /**
47
+ * A key that names a module rather than a frond. A frond's name is a directory name or the
48
+ * `fougere.frond` field of a package, and neither can hold a separator — so the two never
49
+ * collide, and no allow-list of short names has to be kept anywhere.
50
+ */
51
+ export function statesModule(key: string): boolean {
52
+ return key.includes('/') || key.startsWith('.');
53
+ }
54
+
55
+ /**
56
+ * Two levels of the cascade, folded. An entry merges by attribute, so an app naming a frond's
57
+ * address keeps the `extends` the workspace gave it — and an app naming a different `extends`
58
+ * replaces it, silently, the way every scalar key of this config replaces.
59
+ */
60
+ export function mergeStated(base: FrondsStated, override: FrondsStated): FrondsStated {
61
+ const merged: Record<string, FrondStated> = { ...base } as Record<string, FrondStated>;
62
+ for (const [key, value] of Object.entries(override)) {
63
+ if (value === undefined) continue;
64
+
65
+ const mine = merged[key];
66
+ merged[key] = typeof value === 'string' || typeof mine === 'string' || mine === undefined
67
+ ? value
68
+ : { ...mine, ...value };
69
+ }
70
+
71
+ return merged;
72
+ }
@@ -0,0 +1,51 @@
1
+ import { statesModule, type FrondAttributes, type FrondStated, type FrondsStated } from './FrondsStated.js';
2
+
3
+ /** One entry of the config, read whole. */
4
+ export interface StatedFrond {
5
+ /** The key as written — a frond name, or a module specifier. */
6
+ key: string;
7
+ /** The frond it inherits code from, absent when it names none. */
8
+ extends?: string;
9
+ /** The entry that named it, for a refusal that says where to look. */
10
+ path: string;
11
+ /** An address for a frond that is elsewhere, an argument for a module. */
12
+ value?: string;
13
+ }
14
+
15
+ /** What an entry says, whichever of its two forms it was written in. */
16
+ function attributesOf(stated: FrondStated, key: string): FrondAttributes {
17
+ if (typeof stated !== 'string') return stated;
18
+
19
+ return statesModule(key) ? { options: stated } : { remote: stated };
20
+ }
21
+
22
+ /**
23
+ * Every frond the config states, those inheriting from nothing first — which is also the order
24
+ * a boot installs them in, since a scope hangs off the one above it.
25
+ *
26
+ * A partition and not a sort: with one level, a frond that inherits has nothing under it, so
27
+ * two passes put every parent before every child without knowing the tree.
28
+ */
29
+ export function statedFronds(stated: FrondsStated | undefined): StatedFrond[] {
30
+ const entries = Object.entries(stated ?? {}) as [string, FrondStated | undefined][];
31
+
32
+ const read = ([key, value]: [string, FrondStated | undefined]): StatedFrond | undefined => {
33
+ if (value === undefined) return undefined;
34
+
35
+ const attributes = attributesOf(value, key);
36
+ const above = attributes.extends;
37
+ const carries = statesModule(key) ? attributes.options : attributes.remote;
38
+
39
+ return {
40
+ key,
41
+ path: above !== undefined ? `${above}.${key}` : key,
42
+ ...(above !== undefined ? { extends: above } : {}),
43
+ ...(carries !== undefined ? { value: carries } : {}),
44
+ };
45
+ };
46
+
47
+ const stands = (one: StatedFrond | undefined): one is StatedFrond => one !== undefined;
48
+ const all = entries.map(read).filter(stands);
49
+
50
+ return [...all.filter((one) => one.extends === undefined), ...all.filter((one) => one.extends !== undefined)];
51
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The fronds a config names by MODULE rather than by directory — `@fougere/log`, or a package
3
+ * of your own. Reading one means importing it, which is why this lives on the node entry and
4
+ * never on the one a Worker runs.
5
+ *
6
+ * What a module hands back decides where it goes: a shape stating `up` or `down` is an
7
+ * extension and rises with the app, anything else is a frond and is installed like the rest.
8
+ * The author of a config does not have to know which one their package exports — the same
9
+ * reading `extensions/` already does off a module's form.
10
+ */
11
+ import type { Extension } from './boot/Extension.js';
12
+ import type { FrondDescriptor } from './descriptor/FrondDescriptor.js';
13
+ import { statesModule, type FrondsStated } from './FrondsStated.js';
14
+ import { statedFronds } from './StatedFrond.js';
15
+ import { getModuleLoader } from './loader.js';
16
+
17
+ /** The export a specifier reaches: its last segment, `default` failing that. */
18
+ function exportedBy(specifier: string): string {
19
+ const last = specifier.split('/').at(-1) ?? specifier;
20
+
21
+ return last.replace(/\.[cm]?[jt]s$/, '');
22
+ }
23
+
24
+ function rises(built: unknown): built is Extension {
25
+ const shape = built as { up?: unknown; down?: unknown } | null;
26
+
27
+ return typeof shape?.up === 'function' || typeof shape?.down === 'function';
28
+ }
29
+
30
+ /** What a config's module keys hand over, sorted by what each one turned out to be. */
31
+ export async function statedModules(
32
+ stated: FrondsStated | undefined,
33
+ ): Promise<{ fronds: FrondDescriptor[]; extensions: Extension[] }> {
34
+ const fronds: FrondDescriptor[] = [];
35
+ const extensions: Extension[] = [];
36
+ const loader = getModuleLoader();
37
+
38
+ for (const entry of statedFronds(stated)) {
39
+ if (!statesModule(entry.key)) continue;
40
+
41
+ const module = await loader(entry.key) as Record<string, unknown>;
42
+ const name = exportedBy(entry.key);
43
+ const factory = module[name] ?? module.default;
44
+ if (typeof factory !== 'function') {
45
+ throw new Error(
46
+ `Fougere config: '${entry.key}' exports no '${name}'. A module a config names hands its `
47
+ + `frond back from the export its last segment spells — \`export const ${name} = …\` — or `
48
+ + 'from its default.',
49
+ );
50
+ }
51
+
52
+ const built = (factory as (argument?: unknown) => unknown)(entry.value);
53
+ if (rises(built)) extensions.push(built);
54
+ else fronds.push(built as FrondDescriptor);
55
+ }
56
+
57
+ return { fronds, extensions };
58
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Container } from '@fougere/container';
2
2
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
3
+ import type { FrondsStated } from '../FrondsStated.js';
3
4
  import type { ScanResult } from '../scan.js';
4
5
  import type { StorageFactory } from '../storage/StorageFactory.js';
5
6
  import type { Constraint } from '../Constraint.js';
@@ -11,8 +12,13 @@ import type { App } from './App.js';
11
12
 
12
13
  /** Options for createApp(). */
13
14
  export interface CreateAppOptions {
14
- /** Factory function to create the container. Required. */
15
- createContainer: () => Container;
15
+ /**
16
+ * The container this app resolves through. Absent, core builds its own — the only
17
+ * realization there is. A host states one to fill the root BEFORE the boot runs: the CLI
18
+ * registers its terminal that way, and a seed resolving a handler during the ascent would
19
+ * never see a value registered after `createApp` returned.
20
+ */
21
+ createContainer?: () => Container;
16
22
  /** Factory to auto-generate Storage for each scanned entity. */
17
23
  storageFactory?: StorageFactory;
18
24
  /**
@@ -41,6 +47,17 @@ export interface CreateAppOptions {
41
47
  scan?: ScanResult | (() => Promise<ScanResult> | ScanResult);
42
48
  /** What this app STATES it hosts — `frond('blog', { entities: [Post] })`. */
43
49
  fronds?: readonly FrondDescriptor[];
50
+ /**
51
+ * Who inherits code from whom — `FougereConfig.fronds`, handed over whole rather than
52
+ * flattened, because a refusal names where an entry sits in it (`shop.cart`). The host has
53
+ * already turned its addresses and its modules into `remotes` and `fronds`.
54
+ */
55
+ under?: FrondsStated;
56
+ /**
57
+ * This process carries a subset of what the project declares — `only:` at the host. A name
58
+ * the tree states and this process left out is then expected, not a typo.
59
+ */
60
+ narrowed?: boolean;
44
61
  /**
45
62
  * Remote fronds — label → address. What each remote hosts is discovered
46
63
  * at the first miss (rpc.discover), never declared here.
@@ -223,17 +223,30 @@ export class Emissions {
223
223
  return [];
224
224
  }
225
225
 
226
+ return this.answersOf(fact, handed);
227
+ }
228
+
229
+ /**
230
+ * A subscriber that did not answer REFUSES the announcement, it does not shrink it: an
231
+ * announcer handed the survivors cannot tell three answers from two, and its own law then
232
+ * reads silence as consent — the room one would have refused gets booked.
233
+ *
234
+ * What answers NOTHING contributes nothing: a subscriber is free to have no opinion, and
235
+ * `Promise<void>` is how it says so.
236
+ */
237
+ private async answersOf(
238
+ fact: string,
239
+ handed: (Listener & { done: Promise<unknown> })[],
240
+ ): Promise<unknown[]> {
226
241
  const settled = await Promise.allSettled(handed.map((one) => one.done));
227
242
 
228
- // A subscriber that did not answer REFUSES the announcement, it does not shrink it: an
229
- // announcer handed the survivors cannot tell three answers from two, and its own law
230
- // then reads silence as consent — the room one would have refused gets booked.
231
243
  const missing = settled.flatMap((result, at) =>
232
244
  (result.status === 'rejected' ? [{ ...handed[at]!, reason: result.reason as unknown }] : []));
233
245
  if (missing.length > 0) {
234
246
  for (const { facade, op, reason } of missing) {
235
247
  this.log.error(`${fact} → ${facade}.${op}`, this.describeRefusal(fact, reason) ?? reason);
236
248
  }
249
+
237
250
  throw new AggregateError(
238
251
  missing.map((one) => one.reason),
239
252
  `${fact} — ${missing.length} of ${handed.length} subscriber(s) did not answer`
@@ -243,8 +256,6 @@ export class Emissions {
243
256
  );
244
257
  }
245
258
 
246
- // What answers NOTHING contributes nothing — a subscriber is free to have no opinion,
247
- // and `Promise<void>` is how it says so.
248
259
  return settled.flatMap((result) =>
249
260
  (result.status === 'fulfilled' && result.value != null ? [result.value] : []));
250
261
  }