@fougere/core 0.12.4-alpha.0 → 0.12.6-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 (130) hide show
  1. package/dist/FougereConfig.d.ts +2 -3
  2. package/dist/FougereConfig.d.ts.map +1 -1
  3. package/dist/FougereConfig.js.map +1 -1
  4. package/dist/FrondDeclaration.d.ts +5 -0
  5. package/dist/FrondDeclaration.d.ts.map +1 -1
  6. package/dist/FrondDeclaration.js +3 -0
  7. package/dist/FrondDeclaration.js.map +1 -1
  8. package/dist/FrondsStated.d.ts +4 -2
  9. package/dist/FrondsStated.d.ts.map +1 -1
  10. package/dist/FrondsStated.js.map +1 -1
  11. package/dist/StatedFrond.d.ts +3 -0
  12. package/dist/StatedFrond.d.ts.map +1 -1
  13. package/dist/StatedFrond.js +4 -0
  14. package/dist/StatedFrond.js.map +1 -1
  15. package/dist/StatedModules.d.ts +5 -2
  16. package/dist/StatedModules.d.ts.map +1 -1
  17. package/dist/StatedModules.js +11 -6
  18. package/dist/StatedModules.js.map +1 -1
  19. package/dist/boot/AppLifecycle.d.ts +3 -0
  20. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  21. package/dist/boot/AppLifecycle.js +5 -0
  22. package/dist/boot/AppLifecycle.js.map +1 -1
  23. package/dist/boot/Extension.d.ts +6 -0
  24. package/dist/boot/Extension.d.ts.map +1 -1
  25. package/dist/boot/apply.d.ts.map +1 -1
  26. package/dist/boot/apply.js +9 -1
  27. package/dist/boot/apply.js.map +1 -1
  28. package/dist/boot/bootstrap.d.ts.map +1 -1
  29. package/dist/boot/bootstrap.js +14 -5
  30. package/dist/boot/bootstrap.js.map +1 -1
  31. package/dist/boot/install.d.ts +3 -0
  32. package/dist/boot/install.d.ts.map +1 -1
  33. package/dist/boot/install.js +1 -0
  34. package/dist/boot/install.js.map +1 -1
  35. package/dist/boot/nesting.d.ts.map +1 -1
  36. package/dist/boot/nesting.js +2 -1
  37. package/dist/boot/nesting.js.map +1 -1
  38. package/dist/boot/remote.d.ts +2 -1
  39. package/dist/boot/remote.d.ts.map +1 -1
  40. package/dist/boot/remote.js +7 -4
  41. package/dist/boot/remote.js.map +1 -1
  42. package/dist/boot/seed.d.ts +2 -1
  43. package/dist/boot/seed.d.ts.map +1 -1
  44. package/dist/boot/seed.js +10 -29
  45. package/dist/boot/seed.js.map +1 -1
  46. package/dist/contract.d.ts +1 -1
  47. package/dist/contract.d.ts.map +1 -1
  48. package/dist/contract.js +1 -1
  49. package/dist/contract.js.map +1 -1
  50. package/dist/descriptor/ExtensionEntry.d.ts +4 -0
  51. package/dist/descriptor/ExtensionEntry.d.ts.map +1 -1
  52. package/dist/descriptor/ExtensionEntry.js +6 -1
  53. package/dist/descriptor/ExtensionEntry.js.map +1 -1
  54. package/dist/descriptor/FrondDescriptor.d.ts +2 -0
  55. package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
  56. package/dist/descriptor/FrondDescriptor.js +5 -0
  57. package/dist/descriptor/FrondDescriptor.js.map +1 -1
  58. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  59. package/dist/dispatch/ArgumentResolver.js +2 -1
  60. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  61. package/dist/dispatch/Facade.d.ts +3 -0
  62. package/dist/dispatch/Facade.d.ts.map +1 -1
  63. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  64. package/dist/dispatch/HandlerFacade.js +8 -4
  65. package/dist/dispatch/HandlerFacade.js.map +1 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/prefab/collector.d.ts +6 -0
  71. package/dist/prefab/collector.d.ts.map +1 -1
  72. package/dist/prefab/collector.js +26 -1
  73. package/dist/prefab/collector.js.map +1 -1
  74. package/dist/storage/Storage.d.ts +3 -2
  75. package/dist/storage/Storage.d.ts.map +1 -1
  76. package/dist/storage/Storage.js +23 -10
  77. package/dist/storage/Storage.js.map +1 -1
  78. package/dist/storage/Store.d.ts.map +1 -1
  79. package/dist/storage/Store.js +4 -2
  80. package/dist/storage/Store.js.map +1 -1
  81. package/dist/wire/BodyLimit.d.ts +14 -0
  82. package/dist/wire/BodyLimit.d.ts.map +1 -0
  83. package/dist/wire/BodyLimit.js +25 -0
  84. package/dist/wire/BodyLimit.js.map +1 -0
  85. package/dist/wire/Invocation.d.ts +2 -1
  86. package/dist/wire/Invocation.d.ts.map +1 -1
  87. package/dist/wire/Invocation.js +4 -1
  88. package/dist/wire/Invocation.js.map +1 -1
  89. package/dist/wire/InvocationContext.d.ts +2 -0
  90. package/dist/wire/InvocationContext.d.ts.map +1 -1
  91. package/dist/wire/OperationContext.d.ts +2 -0
  92. package/dist/wire/OperationContext.d.ts.map +1 -1
  93. package/dist/wire/SignedCall.d.ts +0 -2
  94. package/dist/wire/SignedCall.d.ts.map +1 -1
  95. package/dist/wire/SignedCall.js +1 -2
  96. package/dist/wire/SignedCall.js.map +1 -1
  97. package/dist/wire/StateShape.d.ts +27 -0
  98. package/dist/wire/StateShape.d.ts.map +1 -0
  99. package/dist/wire/StateShape.js +59 -0
  100. package/dist/wire/StateShape.js.map +1 -0
  101. package/package.json +4 -4
  102. package/src/FougereConfig.ts +2 -3
  103. package/src/FrondDeclaration.ts +6 -0
  104. package/src/FrondsStated.ts +4 -2
  105. package/src/StatedFrond.ts +5 -0
  106. package/src/StatedModules.ts +17 -9
  107. package/src/boot/AppLifecycle.ts +6 -0
  108. package/src/boot/Extension.ts +6 -0
  109. package/src/boot/apply.ts +11 -1
  110. package/src/boot/bootstrap.ts +16 -5
  111. package/src/boot/install.ts +4 -0
  112. package/src/boot/nesting.ts +2 -1
  113. package/src/boot/remote.ts +8 -3
  114. package/src/boot/seed.ts +11 -36
  115. package/src/contract.ts +1 -1
  116. package/src/descriptor/ExtensionEntry.ts +16 -1
  117. package/src/descriptor/FrondDescriptor.ts +7 -0
  118. package/src/dispatch/ArgumentResolver.ts +2 -1
  119. package/src/dispatch/Facade.ts +3 -0
  120. package/src/dispatch/HandlerFacade.ts +8 -3
  121. package/src/index.ts +1 -1
  122. package/src/prefab/collector.ts +29 -1
  123. package/src/storage/Storage.ts +26 -12
  124. package/src/storage/Store.ts +4 -2
  125. package/src/wire/BodyLimit.ts +28 -0
  126. package/src/wire/Invocation.ts +3 -1
  127. package/src/wire/InvocationContext.ts +2 -0
  128. package/src/wire/OperationContext.ts +2 -0
  129. package/src/wire/SignedCall.ts +0 -3
  130. package/src/wire/StateShape.ts +76 -0
@@ -0,0 +1,59 @@
1
+ import { InputValidator, dotted, optional } from '@fougere/schema';
2
+ import { ErrorCode } from './ErrorCode.js';
3
+ import { FougereError } from './FougereError.js';
4
+ /**
5
+ * What a call's `state` may hold in this process: the members its extensions declare, each one
6
+ * absent until a host fills it. Judged at the facade, so a member arriving over the wire is
7
+ * rebuilt by its field exactly as it would have been handed over in-process.
8
+ *
9
+ * Documented: [collectors](https://fougere.dev/docs/business/collectors).
10
+ */
11
+ export class StateShape {
12
+ fields;
13
+ static empty = new StateShape({});
14
+ validator;
15
+ constructor(fields) {
16
+ this.fields = fields;
17
+ this.validator = InputValidator.of(fields);
18
+ }
19
+ /** Two extensions declaring one member refuse: the field that judges it would depend on wiring order. */
20
+ static of(declarations) {
21
+ const fields = {};
22
+ const owners = new Map();
23
+ for (const { name, state } of declarations) {
24
+ for (const [member, field] of Object.entries(state ?? {})) {
25
+ const owner = owners.get(member);
26
+ if (owner) {
27
+ throw new Error(`[state] '${member}' is declared by two extensions, '${owner}' and '${name}'.\n`
28
+ + ' A member has one owner — keep one of them out of `extensions:`.');
29
+ }
30
+ owners.set(member, name);
31
+ fields[member] = optional(field);
32
+ }
33
+ }
34
+ return new StateShape(fields);
35
+ }
36
+ /**
37
+ * The members written since `entered` rebuilt by their fields, or a refusal naming the member
38
+ * and what this process declares. What `entered` already held was judged where it entered.
39
+ */
40
+ judge(state, entity, operation, entered = {}) {
41
+ const written = Object.fromEntries(Object.entries(state).filter(([member, value]) => entered[member] !== value));
42
+ if (Object.keys(written).length === 0)
43
+ return state;
44
+ const result = this.validator.validate(written);
45
+ if (result.success)
46
+ return { ...state, ...result.data };
47
+ const declared = Object.keys(this.fields);
48
+ const errors = result.errors.map((error) => ({ ...error, path: ['state', ...error.path] }));
49
+ throw new FougereError({
50
+ code: ErrorCode.VALIDATION_FAILED,
51
+ message: errors.map((error) => `${dotted(error.path)}: ${error.message}`).join(', ')
52
+ + (declared.length ? ` — this process declares ${declared.join(', ')}` : ' — this process declares no state member'),
53
+ details: errors,
54
+ entity,
55
+ operation,
56
+ });
57
+ }
58
+ }
59
+ //# sourceMappingURL=StateShape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StateShape.js","sourceRoot":"","sources":["../../src/wire/StateShape.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/core",
3
- "version": "0.12.4-alpha.0",
3
+ "version": "0.12.6-alpha.0",
4
4
  "description": "The core — scan, call contract, façades, remote stand-ins.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -60,13 +60,13 @@
60
60
  ],
61
61
  "dependencies": {
62
62
  "dequal": "^2.0.3",
63
- "@fougere/container": "0.12.4-alpha.0",
64
- "@fougere/schema": "0.12.4-alpha.0"
63
+ "@fougere/container": "0.12.6-alpha.0",
64
+ "@fougere/schema": "0.12.6-alpha.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^22.0.0",
68
68
  "@typescript/typescript6": "^6.0.2",
69
- "@fougere/compiler": "0.12.4-alpha.0"
69
+ "@fougere/compiler": "0.12.6-alpha.0"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public"
@@ -1,6 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
- import type { Extension } from './boot/Extension.js';
4
3
  import type { LogLevel } from './builtin/LogLevel.js';
5
4
  import type { ConventionsInput } from './ConventionsInput.js';
6
5
  import type { NameOf } from './NameOf.js';
@@ -22,6 +21,8 @@ export interface FougereConfig {
22
21
  conventions?: ConventionsInput;
23
22
  /** How much every logger says. */
24
23
  logLevel?: LogLevel;
24
+ /** How many bytes a caller may send in one call — 2 MiB when unsaid. Consulted at every door. */
25
+ maxBodyBytes?: number;
25
26
  /**
26
27
  * What this app is made of, and who inherits code from whom. The key is a frond name or a
27
28
  * module specifier; nesting says only that a child resolves what its parent declared.
@@ -29,8 +30,6 @@ export interface FougereConfig {
29
30
  fronds?: FrondsStated;
30
31
  /** What answers a port — a name, or the chain from the outside in. */
31
32
  ports?: PortChoice;
32
- /** The auth provider — an extension, which brings the frond its rows live in. */
33
- auth?: Extension;
34
33
  /** Which protocol adapters this app serves. */
35
34
  adapters?: AdapterConfig;
36
35
  }
@@ -8,6 +8,7 @@ import type { MiddlewareEntry } from './descriptor/MiddlewareEntry.js';
8
8
  import type { PresenterEntry } from './descriptor/PresenterEntry.js';
9
9
  import type { ProviderEntry } from './descriptor/ProviderEntry.js';
10
10
  import type { SeedEntry } from './descriptor/SeedEntry.js';
11
+ import type { ExtensionEntry } from './descriptor/ExtensionEntry.js';
11
12
  import { DEFAULT_CONVENTIONS } from './Conventions.js';
12
13
  import { getPresenterFields } from './prefab/presenter.js';
13
14
  import type { OperationContract } from './wire/OperationContract.js';
@@ -73,6 +74,8 @@ export interface FrondDeclaration {
73
74
  scope?: string;
74
75
  /** What `frond.config.ts` states — the third producer of an operation contract. */
75
76
  operationsOverrides?: FrondDescriptor['operationsOverrides'];
77
+ /** What rises with the process serving this frond — the same thing its `extensions/` states. */
78
+ extensions?: (ExtensionEntry['extension'] & { name: string })[];
76
79
  }
77
80
 
78
81
  /** State a frond without reading a disk. */
@@ -176,5 +179,8 @@ export function frond(name: string, declared: FrondDeclaration = {}): FrondDescr
176
179
  middlewares,
177
180
  ...(declared.pipes ? { pipes: declared.pipes } : {}),
178
181
  ...(declared.surfaces ? { surfaces: declared.surfaces } : {}),
182
+ ...(declared.extensions
183
+ ? { extensions: declared.extensions.map((extension) => ({ name: extension.name, extension, filePath: '' })) }
184
+ : {}),
179
185
  };
180
186
  }
@@ -1,4 +1,5 @@
1
1
  import type { NameOf } from './NameOf.js';
2
+ import type { FrondDescriptor } from './descriptor/FrondDescriptor.js';
2
3
 
3
4
  /**
4
5
  * What the config says about the fronds this app is made of — where each one comes from, and
@@ -20,9 +21,10 @@ export type FrondsStated = {
20
21
 
21
22
  /**
22
23
  * 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
+ * that answers elsewhere, the argument for a module key — or the frond itself, built by the
25
+ * config's own import: `auth: betterAuth({ … })`.
24
26
  */
25
- export type FrondStated = string | FrondAttributes;
27
+ export type FrondStated = string | FrondAttributes | FrondDescriptor;
26
28
 
27
29
  export interface FrondAttributes {
28
30
  /**
@@ -1,4 +1,5 @@
1
1
  import { statesModule, type FrondAttributes, type FrondStated, type FrondsStated } from './FrondsStated.js';
2
+ import { isFrondDescriptor, type FrondDescriptor } from './descriptor/FrondDescriptor.js';
2
3
 
3
4
  /** One entry of the config, read whole. */
4
5
  export interface StatedFrond {
@@ -10,10 +11,13 @@ export interface StatedFrond {
10
11
  path: string;
11
12
  /** An address for a frond that is elsewhere, an argument for a module. */
12
13
  value?: string;
14
+ /** The frond the config built in place, which the host installs like one it scanned. */
15
+ held?: FrondDescriptor;
13
16
  }
14
17
 
15
18
  /** What an entry says, whichever of its two forms it was written in. */
16
19
  function attributesOf(stated: FrondStated, key: string): FrondAttributes {
20
+ if (isFrondDescriptor(stated)) return {};
17
21
  if (typeof stated !== 'string') return stated;
18
22
 
19
23
  return statesModule(key) ? { options: stated } : { remote: stated };
@@ -41,6 +45,7 @@ export function statedFronds(stated: FrondsStated | undefined): StatedFrond[] {
41
45
  path: above !== undefined ? `${above}.${key}` : key,
42
46
  ...(above !== undefined ? { extends: above } : {}),
43
47
  ...(carries !== undefined ? { value: carries } : {}),
48
+ ...(isFrondDescriptor(value) ? { held: value } : {}),
44
49
  };
45
50
  };
46
51
 
@@ -3,13 +3,17 @@
3
3
  * of your own. Reading one means importing it, which is why this lives on the node entry and
4
4
  * never on the one a Worker runs.
5
5
  *
6
- * What a module hands back decides where it goes: a shape stating `up` or `down` is an
6
+ * What a module hands back decides where it goes: a shape stating `up`, `down` or `state` is an
7
7
  * extension and rises with the app, anything else is a frond and is installed like the rest.
8
8
  * The author of a config does not have to know which one their package exports — the same
9
9
  * reading `extensions/` already does off a module's form.
10
+ *
11
+ * A frond the config BUILT in place — `auth: betterAuth({ … })` — is handed over the same way,
12
+ * with nothing to import: the config file already did.
10
13
  */
11
14
  import type { Extension } from './boot/Extension.js';
12
15
  import type { FrondDescriptor } from './descriptor/FrondDescriptor.js';
16
+ import { isExtension } from './descriptor/ExtensionEntry.js';
13
17
  import { statesModule, type FrondsStated } from './FrondsStated.js';
14
18
  import { statedFronds } from './StatedFrond.js';
15
19
  import { getModuleLoader } from './loader.js';
@@ -21,13 +25,7 @@ function exportedBy(specifier: string): string {
21
25
  return last.replace(/\.[cm]?[jt]s$/, '');
22
26
  }
23
27
 
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. */
28
+ /** What a config hands over as code — its module keys and the fronds it built — sorted by what each one is. */
31
29
  export async function statedModules(
32
30
  stated: FrondsStated | undefined,
33
31
  ): Promise<{ fronds: FrondDescriptor[]; extensions: Extension[] }> {
@@ -36,6 +34,16 @@ export async function statedModules(
36
34
  const loader = getModuleLoader();
37
35
 
38
36
  for (const entry of statedFronds(stated)) {
37
+ if (entry.held) {
38
+ if (entry.held.name !== entry.key) {
39
+ throw new Error(
40
+ `Fougere config: the entry '${entry.path}' holds the frond '${entry.held.name}'. `
41
+ + `Name the entry '${entry.held.name}' — the key is how \`only:\` and an address find it.`,
42
+ );
43
+ }
44
+ fronds.push(entry.held);
45
+ continue;
46
+ }
39
47
  if (!statesModule(entry.key)) continue;
40
48
 
41
49
  const module = await loader(entry.key) as Record<string, unknown>;
@@ -50,7 +58,7 @@ export async function statedModules(
50
58
  }
51
59
 
52
60
  const built = (factory as (argument?: unknown) => unknown)(entry.value);
53
- if (rises(built)) extensions.push(built);
61
+ if (isExtension(built)) extensions.push(built as Extension);
54
62
  else fronds.push(built as FrondDescriptor);
55
63
  }
56
64
 
@@ -1,5 +1,6 @@
1
1
  import type { App } from './App.js';
2
2
  import type { Extension } from './Extension.js';
3
+ import { StateShape } from '../wire/StateShape.js';
3
4
 
4
5
  /** Runs application extensions up in order and down in reverse order. */
5
6
  export class AppLifecycle {
@@ -20,6 +21,11 @@ export class AppLifecycle {
20
21
  return this.members.map((extension) => extension.name);
21
22
  }
22
23
 
24
+ /** Read off the members that stay, so an extension replaced by name declares nothing. */
25
+ state(): StateShape {
26
+ return StateShape.of(this.members);
27
+ }
28
+
23
29
  async up(app: App): Promise<void> {
24
30
  for (const extension of this.members) await extension.up?.(app);
25
31
  }
@@ -1,4 +1,5 @@
1
1
  import type { App } from './App.js';
2
+ import type { Field } from '@fougere/schema';
2
3
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
3
4
 
4
5
  /**
@@ -15,6 +16,11 @@ export interface Extension {
15
16
  * signature is the only way to ask for one.
16
17
  */
17
18
  fronds?: readonly FrondDescriptor[];
19
+ /**
20
+ * The members it puts on a call's `state` — `{ user: json(User) }`. A member nobody declares
21
+ * is refused at the facade, and one declared twice refuses the boot.
22
+ */
23
+ state?: Readonly<Record<string, Field>>;
18
24
  up?(app: App): void | Promise<void>;
19
25
  down?(app: App): void | Promise<void>;
20
26
  }
package/src/boot/apply.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { FougereConfig } from '../FougereConfig.js';
2
2
  import { envLevel, logLevel, setLogLevel } from '../builtin/Logger.js';
3
+ import { maxBodyBytes, setMaxBodyBytes } from '../wire/BodyLimit.js';
3
4
  import { dequal } from 'dequal';
4
5
 
5
6
  export interface ConfigApplication {
@@ -9,6 +10,9 @@ export interface ConfigApplication {
9
10
  pending: string[];
10
11
  }
11
12
 
13
+ /** The keys read at use rather than consumed at boot, so a re-read moves them. */
14
+ const CONSULTED = new Set(['logLevel', 'maxBodyBytes']);
15
+
12
16
  /**
13
17
  * What a (re-)read config changes in a process that is already running.
14
18
  *
@@ -34,8 +38,14 @@ export function applyConfig(next: FougereConfig, inForce?: FougereConfig): Confi
34
38
  applied.push(`logLevel: ${before} → ${wanted}`);
35
39
  }
36
40
 
41
+ const limit = next.maxBodyBytes ?? maxBodyBytes();
42
+ if (limit !== maxBodyBytes()) {
43
+ applied.push(`maxBodyBytes: ${maxBodyBytes()} → ${limit}`);
44
+ setMaxBodyBytes(limit);
45
+ }
46
+
37
47
  for (const key of new Set([...Object.keys(next), ...Object.keys(inForce ?? {})])) {
38
- if (key === 'logLevel' || !inForce) continue;
48
+ if (CONSULTED.has(key) || !inForce) continue;
39
49
  if (!dequal((next as Record<string, unknown>)[key], (inForce as Record<string, unknown>)[key])) {
40
50
  pending.push(key);
41
51
  }
@@ -568,16 +568,26 @@ function markLineCarriers(emissions: Emissions): void {
568
568
  * declares `product`.
569
569
  */
570
570
  function answerRemotes(
571
- { container, routeRegistry, dispatcher, getMiddlewares }: Pick<Assembly, 'container' | 'routeRegistry' | 'dispatcher' | 'getMiddlewares'>,
571
+ { container, routeRegistry, dispatcher, getMiddlewares, options, state }: Pick<Assembly, 'container' | 'routeRegistry' | 'dispatcher' | 'getMiddlewares' | 'options' | 'state'>,
572
572
  remoteRouter: RemoteRouter,
573
+ fronds: Fronds,
573
574
  ): void {
574
- container.setFallback((name) => (isFacadeKey(name) ? facadeOperations(dispatcher, addressOf(name)) : undefined));
575
+ // Only for what another process may serve: an entity a frond hosts HERE and gives no handler
576
+ // is served by nobody, and a stand-in for it had GraphQL project an entity with no operation.
577
+ const servedElsewhere = (address: string): boolean => {
578
+ const owner = fronds.owner(address);
579
+
580
+ return owner === undefined || owner.name in (options.remotes ?? {});
581
+ };
582
+ container.setFallback((name) => (isFacadeKey(name) && servedElsewhere(addressOf(name))
583
+ ? facadeOperations(dispatcher, addressOf(name))
584
+ : undefined));
575
585
 
576
586
  const remoteFacades = new Map<string, Record<string, Function>>();
577
587
  routeRegistry.addResolver(remoteRoutes((entity) => {
578
588
  const known = remoteFacades.get(entity);
579
589
  if (known) return known;
580
- const facade = createRemoteFacade(entity, remoteRouter, getMiddlewares);
590
+ const facade = createRemoteFacade(entity, remoteRouter, getMiddlewares, state);
581
591
  remoteFacades.set(entity, facade);
582
592
 
583
593
  return facade;
@@ -650,6 +660,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
650
660
  log.debug('builtins registered (Logger, Config)');
651
661
 
652
662
  const { fronds, operationModel } = await readFronds(options, log);
663
+ appLifecycle.add(...frondExtensions(fronds));
653
664
  const remoteRouter = remoteRouterOf(options);
654
665
  const { inflight, routeRegistry, dispatchLifecycle, dispatcher, localDispatcher, getMiddlewares, use } =
655
666
  dispatching(options, log, fronds, () => journalOf());
@@ -672,6 +683,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
672
683
  container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
673
684
  boundPorts, refused, relations, hosting, operationModel, entityByName, frondOf, contractsOf,
674
685
  getMiddlewares, use, middlewaresOf: new Map(), seamsOf: new Map(), log, options,
686
+ state: appLifecycle.state(),
675
687
  };
676
688
  for (const frond of fronds) await installFrond(frond, assembly);
677
689
 
@@ -685,7 +697,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
685
697
  refuseWhatDoesNotHold(refused);
686
698
  markLineCarriers(emissions);
687
699
 
688
- if (remoteRouter) answerRemotes(assembly, remoteRouter);
700
+ if (remoteRouter) answerRemotes(assembly, remoteRouter, fronds);
689
701
 
690
702
  const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
691
703
  container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log,
@@ -725,7 +737,6 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
725
737
  serveCoreRpc(app, hosting, storageFor);
726
738
 
727
739
  built = app;
728
- appLifecycle.add(...frondExtensions(fronds));
729
740
  await appLifecycle.up(app);
730
741
  stopAnnouncing = announceLines(emissions, container, carry);
731
742
 
@@ -1,5 +1,6 @@
1
1
  /** Putting one frond into the app being built: its scope, what it serves, what it takes. */
2
2
  import type { Container } from '@fougere/container';
3
+ import type { StateShape } from '../wire/StateShape.js';
3
4
  import type { ProviderEntry } from '../descriptor/ProviderEntry.js';
4
5
  import type { PresenterEntry } from '../descriptor/PresenterEntry.js';
5
6
  import { lowerFirst, type Fields, type SchemaView } from '@fougere/schema';
@@ -85,6 +86,8 @@ export interface Assembly {
85
86
  seamsOf: Map<string, Map<string, ProviderEntry[]>>;
86
87
  log: Logger;
87
88
  options: CreateAppOptions;
89
+ /** What a call's `state` may hold, read off every extension this process mounts. */
90
+ state: StateShape;
88
91
  }
89
92
 
90
93
  /**
@@ -424,6 +427,7 @@ function buildFacadeInto(
424
427
  presenter: presenterMap.get(handler.address),
425
428
  presenterScope: scope,
426
429
  middlewares: () => getMiddlewares(handler.address),
430
+ state: assembly.state,
427
431
  });
428
432
 
429
433
  // Emissions use the same contracts and execution path as direct calls, and the terms sit
@@ -6,6 +6,7 @@
6
6
  * Documented: [fronds](https://fougere.dev/docs/infra/fronds).
7
7
  */
8
8
  import type { Diagnostic } from '../diagnostic.js';
9
+ import { isFrondDescriptor } from '../descriptor/FrondDescriptor.js';
9
10
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
10
11
  import { Fronds } from '../descriptor/Fronds.js';
11
12
  import { statesModule, type FrondsStated } from '../FrondsStated.js';
@@ -102,7 +103,7 @@ export function nestingOf(
102
103
  if (frond === undefined) continue;
103
104
 
104
105
  const placed = stated[parent];
105
- const address = typeof placed === 'string' ? placed : placed?.remote;
106
+ const address = typeof placed === 'string' ? placed : isFrondDescriptor(placed) ? undefined : placed?.remote;
106
107
  if (address !== undefined || (remotes && parent in remotes)) {
107
108
  refused.push({
108
109
  severity: 'blocking',
@@ -4,6 +4,7 @@
4
4
  * Documented: [the gradient](https://fougere.dev/docs/infra/gradient).
5
5
  */
6
6
  import type { FrondCall } from '../wire/FrondCall.js';
7
+ import type { StateShape } from '../wire/StateShape.js';
7
8
  import type { Transport } from '../wire/Transport.js';
8
9
  import { RPC_ENTITY } from '../wire/RpcAnswer.js';
9
10
  import { assertIdentityCard } from '../wire/card/IdentityCard.js';
@@ -103,15 +104,19 @@ export function createRemoteFacade(
103
104
  entity: string,
104
105
  router: RemoteRouter,
105
106
  middlewaresFor: (address: string) => AppMiddleware[],
107
+ shape: StateShape,
106
108
  ): Facade {
107
- const opFn = (op: string) => async (invocation: InvocationContext = Invocation.empty) => {
109
+ const opFn = (op: string) => async (received: InvocationContext = Invocation.empty) => {
108
110
  const { frond, transport, schema } = await router.route(entity);
109
111
  const call: FrondCall = { frond, entity, op };
112
+ const state = received.crossed ? { ...received.state } : shape.judge(received.state, entity, op);
113
+ const invocation = { ...received, state };
114
+ const entered = { ...state };
110
115
  const ctx: OperationContext = {
111
- entity, frond, operation: op, args: [], state: invocation.state, invocation,
116
+ entity, frond, operation: op, args: [], state, invocation, crosses: true,
112
117
  };
113
118
  const answer = await runMiddlewares(middlewaresFor(entity), ctx, () =>
114
- transport(call, ctx.invocation ?? invocation));
119
+ transport(call, { ...(ctx.invocation ?? invocation), state: shape.judge(ctx.state, entity, op, entered) }));
115
120
 
116
121
  // The schema the card carried, put to work: a row crosses as data and comes back through
117
122
  // the same codecs a local facade applies, so a placement does not decide what a caller
package/src/boot/seed.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { pageOf } from '../wire/Page.js';
1
2
  import { Role } from '@fougere/schema';
2
- import { facadeKeyOf } from '../wire/Facade.js';
3
3
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
4
4
  import type { SeedEntry } from '../descriptor/SeedEntry.js';
5
5
  import type { SeedFactory } from '../descriptor/SeedFactory.js';
@@ -64,14 +64,9 @@ function whatEachWaitsFor(
64
64
  }));
65
65
  }
66
66
 
67
- /** Where a seed writes, and what it may skip — resolved per entity. */
68
- interface SeedFacade {
69
- list(): Promise<unknown[]>;
70
- write(item: Record<string, unknown>): Promise<unknown>;
71
- }
72
-
73
67
  /**
74
- * Plant a set of seeds, in the order given.
68
+ * Plant a set of seeds, in the order given — by the storage, as a handler writes: a seed is the
69
+ * frond's own code, not a client.
75
70
  *
76
71
  * Documented: [seeds](https://fougere.dev/docs/business/seeds).
77
72
  */
@@ -81,26 +76,26 @@ export async function runSeeds(
81
76
  report: (message: string) => void = () => {},
82
77
  ): Promise<void> {
83
78
  for (const seed of seeds) {
84
- const resolve = <T>(name: string) => app.resolve<T>(name + 'Handler');
85
- const data = typeof seed.data === 'function' ? await (seed.data as SeedFactory)(resolve) : seed.data;
86
-
87
- const facade = facadeFor(app, seed.entityName);
88
- if (!facade) {
89
- report(` ${seed.entityName}: no handler façade nor storage — skipping seed`);
79
+ const storage = app.storageFor(seed.entityName);
80
+ if (!storage) {
81
+ report(` ${seed.entityName}: no storage in this process — skipping seed`);
90
82
  continue;
91
83
  }
92
84
 
93
- const existing = await facade.list();
85
+ const existing = pageOf(await storage.list()).items;
94
86
  if (existing.length > 0) {
95
87
  report(` ${seed.entityName}: skipped (${existing.length} exist)`);
96
88
  continue;
97
89
  }
98
90
 
91
+ const resolve = <T>(name: string) => app.resolve<T>(name + 'Handler');
92
+ const data = typeof seed.data === 'function' ? await (seed.data as SeedFactory)(resolve) : seed.data;
93
+
99
94
  // The driver's error alone names neither the entity nor the row — and a foreign
100
95
  // key is exactly what a seed gets wrong.
101
96
  for (const item of data) {
102
97
  try {
103
- await facade.write(item);
98
+ await storage.create(item);
104
99
  } catch (cause) {
105
100
  throw new Error(
106
101
  `Seed '${seed.entityName}' failed on ${JSON.stringify(item)}: ${(cause as Error)?.message ?? cause}`,
@@ -112,26 +107,6 @@ export async function runSeeds(
112
107
  }
113
108
  }
114
109
 
115
- /** A seed is not a client. */
116
- function facadeFor(app: App, entityName: string): SeedFacade | undefined {
117
- let handler: Record<string, Function> | undefined;
118
- try { handler = app.resolve<Record<string, Function>>(facadeKeyOf(entityName)); } catch {}
119
-
120
- const list = handler?.list;
121
- const create = handler?.create;
122
- if (typeof list === 'function' && typeof create === 'function') {
123
- return {
124
- list: () => list.call(handler) as Promise<unknown[]>,
125
- write: (item) => create.call(handler, { params: {}, query: {}, input: item, state: {} }),
126
- };
127
- }
128
-
129
- const storage = app.storageFor(entityName);
130
- if (!storage) return undefined;
131
-
132
- return { list: () => storage.list(), write: (item) => storage.create(item) };
133
- }
134
-
135
110
  /**
136
111
  * The framework's own ascent for rows, named — so a host can REPLACE it rather than take over the
137
112
  * whole post-boot to get its own.
package/src/contract.ts CHANGED
@@ -24,7 +24,7 @@ export { RouteAddress } from './wire/RouteAddress.js';
24
24
  export type { FrondCall } from './wire/FrondCall.js';
25
25
  export type { SignedCall } from './wire/SignedCall.js';
26
26
  export type { Transport } from './wire/Transport.js';
27
- export { MAX_BODY_BYTES } from './wire/SignedCall.js';
27
+ export { ENVELOPE_BYTES, maxBodyBytes, maxFrameBytes, setMaxBodyBytes } from './wire/BodyLimit.js';
28
28
  // The reserved entity, VALUE and not type: a consumer that wants to leave it alone — a
29
29
  // call log ignoring its own reader — has to be able to name it.
30
30
  export { RPC_ENTITY } from './wire/RpcAnswer.js';
@@ -1,3 +1,5 @@
1
+ import type { Field } from '@fougere/schema';
2
+
1
3
  /**
2
4
  * An extension this frond declares — what rises and falls with the process, written beside the
3
5
  * code it instruments.
@@ -10,6 +12,19 @@ export interface ExtensionEntry {
10
12
  /** Its own `name`, or the file's — the ascent REPLACES a name already declared. */
11
13
  name: string;
12
14
  /** The value handed to the ascent, the same shape `CreateAppOptions.extensions` takes. */
13
- extension: { name?: string; up?: (app: never) => void | Promise<void>; down?: (app: never) => void | Promise<void> };
15
+ extension: {
16
+ name?: string;
17
+ state?: Readonly<Record<string, Field>>;
18
+ up?: (app: never) => void | Promise<void>;
19
+ down?: (app: never) => void | Promise<void>;
20
+ };
14
21
  filePath: string;
15
22
  }
23
+
24
+ /** Recognized by its FORM: a value stating `up`, `down`, or the `state` members it declares. */
25
+ export function isExtension(value: unknown): value is ExtensionEntry['extension'] {
26
+ const shape = value as { up?: unknown; down?: unknown; state?: unknown } | null;
27
+
28
+ return typeof shape?.up === 'function' || typeof shape?.down === 'function'
29
+ || (typeof shape?.state === 'object' && shape.state !== null);
30
+ }
@@ -63,3 +63,10 @@ export interface FrondDescriptor {
63
63
  rest?: { method?: string; path?: string; status?: number };
64
64
  }>;
65
65
  }
66
+
67
+ /** Recognized by its FORM, like everything a config holds: a name and the lists every frond carries. */
68
+ export function isFrondDescriptor(value: unknown): value is FrondDescriptor {
69
+ const shape = value as Partial<Record<keyof FrondDescriptor, unknown>> | null;
70
+
71
+ return typeof shape?.name === 'string' && Array.isArray(shape.entities) && Array.isArray(shape.handlers);
72
+ }
@@ -1,6 +1,7 @@
1
1
  import type { InvocationContext } from '../wire/InvocationContext.js';
2
2
  import type { BindingPlan } from '../wire/binding.js';
3
3
  import type { CollectorLookup } from './CollectorLookup.js';
4
+ import { collectedAs } from '../prefab/collector.js';
4
5
 
5
6
  /** Resolves an operation's declared binding plan against one invocation. */
6
7
  export class ArgumentResolver {
@@ -13,7 +14,7 @@ export class ArgumentResolver {
13
14
  switch (binding.source.kind) {
14
15
  case 'collector': {
15
16
  const collector = this.collectors?.(binding.source.typeName);
16
- args.push(collector ? await collector.collect(ctx) : undefined);
17
+ args.push(collector ? collectedAs(collector, await collector.collect(ctx)) : undefined);
17
18
  break;
18
19
  }
19
20
  case 'context': {
@@ -3,6 +3,7 @@ import { type AppMiddleware } from '../wire/AppMiddleware.js';
3
3
  import type { EffectiveOperationsMap } from '../EffectiveOperationsMap.js';
4
4
  import type { HandlerEntry } from '../descriptor/HandlerEntry.js';
5
5
  import type { PresenterEntry } from '../descriptor/PresenterEntry.js';
6
+ import type { StateShape } from '../wire/StateShape.js';
6
7
 
7
8
  /** What boot resolved around one handler, beyond the handler and the scope it resolves in. */
8
9
  export interface Facade {
@@ -22,4 +23,6 @@ export interface Facade {
22
23
  presenterScope: Container;
23
24
  /** The middlewares that apply to this address, read at call time and never at boot. */
24
25
  middlewares: () => AppMiddleware[];
26
+ /** What a call's `state` may hold in this process. */
27
+ state: StateShape;
25
28
  }