@fougere/core 0.10.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 (215) hide show
  1. package/dist/CallIdentity.d.ts.map +1 -1
  2. package/dist/CallIdentity.js +2 -0
  3. package/dist/CallIdentity.js.map +1 -1
  4. package/dist/Constraint.d.ts +1 -1
  5. package/dist/Constraint.d.ts.map +1 -1
  6. package/dist/FougereConfig.d.ts +10 -6
  7. package/dist/FougereConfig.d.ts.map +1 -1
  8. package/dist/FougereConfig.js +19 -4
  9. package/dist/FougereConfig.js.map +1 -1
  10. package/dist/FrondConfig.d.ts +0 -6
  11. package/dist/FrondConfig.d.ts.map +1 -1
  12. package/dist/FrondConfig.js.map +1 -1
  13. package/dist/FrondDeclaration.d.ts +2 -9
  14. package/dist/FrondDeclaration.d.ts.map +1 -1
  15. package/dist/FrondDeclaration.js +0 -1
  16. package/dist/FrondDeclaration.js.map +1 -1
  17. package/dist/FrondsStated.d.ts +54 -0
  18. package/dist/FrondsStated.d.ts.map +1 -0
  19. package/dist/FrondsStated.js +26 -0
  20. package/dist/FrondsStated.js.map +1 -0
  21. package/dist/Source.d.ts.map +1 -1
  22. package/dist/Source.js +4 -3
  23. package/dist/Source.js.map +1 -1
  24. package/dist/StatedFrond.d.ts +21 -0
  25. package/dist/StatedFrond.d.ts.map +1 -0
  26. package/dist/StatedFrond.js +34 -0
  27. package/dist/StatedFrond.js.map +1 -0
  28. package/dist/StatedModules.d.ts +19 -0
  29. package/dist/StatedModules.d.ts.map +1 -0
  30. package/dist/StatedModules.js +37 -0
  31. package/dist/StatedModules.js.map +1 -0
  32. package/dist/boot/CreateAppOptions.d.ts +19 -2
  33. package/dist/boot/CreateAppOptions.d.ts.map +1 -1
  34. package/dist/boot/Emissions.d.ts +9 -0
  35. package/dist/boot/Emissions.d.ts.map +1 -1
  36. package/dist/boot/Emissions.js +11 -5
  37. package/dist/boot/Emissions.js.map +1 -1
  38. package/dist/boot/Hosting.d.ts +38 -0
  39. package/dist/boot/Hosting.d.ts.map +1 -0
  40. package/dist/boot/Hosting.js +2 -0
  41. package/dist/boot/Hosting.js.map +1 -0
  42. package/dist/boot/Peer.d.ts +16 -0
  43. package/dist/boot/Peer.d.ts.map +1 -0
  44. package/dist/boot/Peer.js +2 -0
  45. package/dist/boot/Peer.js.map +1 -0
  46. package/dist/boot/bootstrap.d.ts.map +1 -1
  47. package/dist/boot/bootstrap.js +372 -235
  48. package/dist/boot/bootstrap.js.map +1 -1
  49. package/dist/boot/declared.d.ts.map +1 -1
  50. package/dist/boot/declared.js +38 -3
  51. package/dist/boot/declared.js.map +1 -1
  52. package/dist/boot/frame.d.ts +0 -4
  53. package/dist/boot/frame.d.ts.map +1 -1
  54. package/dist/boot/frame.js +31 -14
  55. package/dist/boot/frame.js.map +1 -1
  56. package/dist/boot/hosted.d.ts.map +1 -1
  57. package/dist/boot/hosted.js +4 -3
  58. package/dist/boot/hosted.js.map +1 -1
  59. package/dist/boot/install.d.ts +19 -0
  60. package/dist/boot/install.d.ts.map +1 -1
  61. package/dist/boot/install.js +273 -178
  62. package/dist/boot/install.js.map +1 -1
  63. package/dist/boot/nesting.d.ts +33 -0
  64. package/dist/boot/nesting.d.ts.map +1 -0
  65. package/dist/boot/nesting.js +164 -0
  66. package/dist/boot/nesting.js.map +1 -0
  67. package/dist/boot/ownership.d.ts +0 -1
  68. package/dist/boot/ownership.d.ts.map +1 -1
  69. package/dist/boot/ownership.js +66 -51
  70. package/dist/boot/ownership.js.map +1 -1
  71. package/dist/boot/peerOver.d.ts +4 -0
  72. package/dist/boot/peerOver.d.ts.map +1 -0
  73. package/dist/boot/peerOver.js +12 -0
  74. package/dist/boot/peerOver.js.map +1 -0
  75. package/dist/boot/ports.js +30 -25
  76. package/dist/boot/ports.js.map +1 -1
  77. package/dist/boot/relations.d.ts +54 -0
  78. package/dist/boot/relations.d.ts.map +1 -0
  79. package/dist/boot/relations.js +150 -0
  80. package/dist/boot/relations.js.map +1 -0
  81. package/dist/boot/remote.d.ts.map +1 -1
  82. package/dist/boot/remote.js +31 -28
  83. package/dist/boot/remote.js.map +1 -1
  84. package/dist/boot/seed.d.ts.map +1 -1
  85. package/dist/boot/seed.js +27 -20
  86. package/dist/boot/seed.js.map +1 -1
  87. package/dist/boot/together.d.ts +3 -0
  88. package/dist/boot/together.d.ts.map +1 -1
  89. package/dist/boot/together.js +72 -56
  90. package/dist/boot/together.js.map +1 -1
  91. package/dist/contract.d.ts +1 -0
  92. package/dist/contract.d.ts.map +1 -1
  93. package/dist/contract.js.map +1 -1
  94. package/dist/descriptor/FrondDescriptor.d.ts +7 -0
  95. package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
  96. package/dist/descriptor/MiddlewareEntry.d.ts +6 -5
  97. package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
  98. package/dist/descriptor/ProviderEntry.d.ts +7 -0
  99. package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
  100. package/dist/dispatch/Dependent.d.ts +19 -0
  101. package/dist/dispatch/Dependent.d.ts.map +1 -0
  102. package/dist/dispatch/Dependent.js +2 -0
  103. package/dist/dispatch/Dependent.js.map +1 -0
  104. package/dist/dispatch/Dispatcher.d.ts +14 -1
  105. package/dist/dispatch/Dispatcher.d.ts.map +1 -1
  106. package/dist/dispatch/Dispatcher.js +36 -8
  107. package/dist/dispatch/Dispatcher.js.map +1 -1
  108. package/dist/dispatch/HandlerFacade.d.ts +6 -1
  109. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  110. package/dist/dispatch/HandlerFacade.js +23 -20
  111. package/dist/dispatch/HandlerFacade.js.map +1 -1
  112. package/dist/dispatch/Journal.d.ts +25 -0
  113. package/dist/dispatch/Journal.d.ts.map +1 -0
  114. package/dist/dispatch/Journal.js +3 -0
  115. package/dist/dispatch/Journal.js.map +1 -0
  116. package/dist/dispatch/PresenterExecutor.d.ts +6 -0
  117. package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
  118. package/dist/dispatch/PresenterExecutor.js +30 -22
  119. package/dist/dispatch/PresenterExecutor.js.map +1 -1
  120. package/dist/dispatch/RelationCheck.d.ts +22 -0
  121. package/dist/dispatch/RelationCheck.d.ts.map +1 -0
  122. package/dist/dispatch/RelationCheck.js +2 -0
  123. package/dist/dispatch/RelationCheck.js.map +1 -0
  124. package/dist/dispatch/Release.d.ts +36 -0
  125. package/dist/dispatch/Release.d.ts.map +1 -0
  126. package/dist/dispatch/Release.js +130 -0
  127. package/dist/dispatch/Release.js.map +1 -0
  128. package/dist/dispatch/StorageGuard.d.ts +39 -1
  129. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  130. package/dist/dispatch/StorageGuard.js +128 -54
  131. package/dist/dispatch/StorageGuard.js.map +1 -1
  132. package/dist/index.d.ts +8 -0
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +6 -0
  135. package/dist/index.js.map +1 -1
  136. package/dist/node.d.ts +2 -1
  137. package/dist/node.d.ts.map +1 -1
  138. package/dist/node.js +2 -1
  139. package/dist/node.js.map +1 -1
  140. package/dist/storage/Storage.js +1 -1
  141. package/dist/storage/Storage.js.map +1 -1
  142. package/dist/storage/Store.d.ts.map +1 -1
  143. package/dist/storage/Store.js +29 -21
  144. package/dist/storage/Store.js.map +1 -1
  145. package/dist/verify.d.ts.map +1 -1
  146. package/dist/verify.js +14 -0
  147. package/dist/verify.js.map +1 -1
  148. package/dist/wire/Emit.d.ts +9 -0
  149. package/dist/wire/Emit.d.ts.map +1 -1
  150. package/dist/wire/Emit.js +16 -0
  151. package/dist/wire/Emit.js.map +1 -1
  152. package/dist/wire/Invocation.d.ts +3 -2
  153. package/dist/wire/Invocation.d.ts.map +1 -1
  154. package/dist/wire/Invocation.js +5 -2
  155. package/dist/wire/Invocation.js.map +1 -1
  156. package/dist/wire/InvocationContext.d.ts +2 -0
  157. package/dist/wire/InvocationContext.d.ts.map +1 -1
  158. package/dist/wire/SignedCall.d.ts +2 -0
  159. package/dist/wire/SignedCall.d.ts.map +1 -1
  160. package/dist/wire/binding.d.ts.map +1 -1
  161. package/dist/wire/binding.js +29 -57
  162. package/dist/wire/binding.js.map +1 -1
  163. package/dist/wire/drift.d.ts.map +1 -1
  164. package/dist/wire/drift.js +22 -19
  165. package/dist/wire/drift.js.map +1 -1
  166. package/package.json +4 -4
  167. package/src/CallIdentity.ts +2 -0
  168. package/src/Constraint.ts +1 -1
  169. package/src/FougereConfig.ts +24 -10
  170. package/src/FrondConfig.ts +0 -6
  171. package/src/FrondDeclaration.ts +2 -7
  172. package/src/FrondsStated.ts +72 -0
  173. package/src/Source.ts +3 -2
  174. package/src/StatedFrond.ts +51 -0
  175. package/src/StatedModules.ts +58 -0
  176. package/src/boot/CreateAppOptions.ts +19 -2
  177. package/src/boot/Emissions.ts +16 -5
  178. package/src/boot/Hosting.ts +38 -0
  179. package/src/boot/Peer.ts +15 -0
  180. package/src/boot/bootstrap.ts +512 -271
  181. package/src/boot/declared.ts +38 -3
  182. package/src/boot/frame.ts +42 -11
  183. package/src/boot/hosted.ts +4 -3
  184. package/src/boot/install.ts +400 -203
  185. package/src/boot/nesting.ts +196 -0
  186. package/src/boot/ownership.ts +79 -51
  187. package/src/boot/peerOver.ts +17 -0
  188. package/src/boot/ports.ts +45 -31
  189. package/src/boot/relations.ts +172 -0
  190. package/src/boot/remote.ts +47 -29
  191. package/src/boot/seed.ts +36 -27
  192. package/src/boot/together.ts +102 -63
  193. package/src/contract.ts +1 -0
  194. package/src/descriptor/FrondDescriptor.ts +7 -0
  195. package/src/descriptor/MiddlewareEntry.ts +6 -5
  196. package/src/descriptor/ProviderEntry.ts +7 -0
  197. package/src/dispatch/Dependent.ts +19 -0
  198. package/src/dispatch/Dispatcher.ts +43 -7
  199. package/src/dispatch/HandlerFacade.ts +27 -17
  200. package/src/dispatch/Journal.ts +26 -0
  201. package/src/dispatch/PresenterExecutor.ts +33 -22
  202. package/src/dispatch/RelationCheck.ts +21 -0
  203. package/src/dispatch/Release.ts +180 -0
  204. package/src/dispatch/StorageGuard.ts +138 -55
  205. package/src/index.ts +8 -0
  206. package/src/node.ts +2 -1
  207. package/src/storage/Storage.ts +2 -2
  208. package/src/storage/Store.ts +39 -18
  209. package/src/verify.ts +15 -0
  210. package/src/wire/Emit.ts +23 -0
  211. package/src/wire/Invocation.ts +5 -2
  212. package/src/wire/InvocationContext.ts +2 -0
  213. package/src/wire/SignedCall.ts +2 -0
  214. package/src/wire/binding.ts +26 -58
  215. package/src/wire/drift.ts +48 -21
@@ -1,6 +1,12 @@
1
+ import { createContainer } from '@fougere/container';
1
2
  import { lowerFirst } from '@fougere/schema';
2
3
  import { hostedBy } from './hosted.js';
4
+ import { nestingOf, parentsFirst } from './nesting.js';
3
5
  import { installFrond } from './install.js';
6
+ import { dependentsOf, releasing, unfinishable, unheldAmong } from './relations.js';
7
+ import { release as releaseRow } from '../dispatch/Release.js';
8
+ import { peerOver } from './peerOver.js';
9
+ import { JOURNAL } from '../dispatch/Journal.js';
4
10
  import { refusalOf } from '../diagnostic.js';
5
11
  import { Carry } from '../builtin/Carry.js';
6
12
  import { Logger } from '../builtin/Logger.js';
@@ -13,6 +19,9 @@ import { createRemoteRouter, createRemoteFacade } from './remote.js';
13
19
  import { Emissions } from './Emissions.js';
14
20
  import { resolveEffectiveOperations } from '../EffectiveOperationModel.js';
15
21
  import { InFlight } from '../dispatch/InFlight.js';
22
+ // The keys, each read from where its concept is declared — never respelled here.
23
+ import { RPC_ENTITY } from '../wire/RpcAnswer.js';
24
+ import { Invocation } from '../wire/Invocation.js';
16
25
  import { facadeKeyOf } from '../wire/Facade.js';
17
26
  import { identityCardOf } from './card.js';
18
27
  import { AppLifecycle, migrating } from './AppLifecycle.js';
@@ -63,9 +72,334 @@ function keyClaims(fronds, remotes, refused) {
63
72
  claim(presenterKeyOf(presenter.entityName), frond.name, 'presenter', presenter.filePath);
64
73
  }
65
74
  }
75
+ /**
76
+ * What this app hosts, and what its operations effectively are.
77
+ *
78
+ * Producing it may read a disk; consuming it never does, which is the whole reason this file
79
+ * names no builtin and a Worker can run what it builds.
80
+ */
81
+ async function readFronds(options, log) {
82
+ const scanStart = performance.now();
83
+ // An extension's fronds sit beside the app's own: it is installed like any other, and
84
+ // its handlers resolve at call time — by which point the extension's `up` has put what
85
+ // they ask for in the container.
86
+ const brought = (options.extensions ?? [])
87
+ .flatMap((extension) => extension?.fronds ?? [])
88
+ .map((frond) => ({ ...frond, brought: true }));
89
+ const { fronds, diagnostics } = await hostedBy(brought.length > 0 ? { ...options, fronds: [...(options.fronds ?? []), ...brought] } : options);
90
+ // An app that states nothing AND scans nothing is a mistake — unless something else it
91
+ // declares brings its own entities, which an auth provider does. Refused here and not in
92
+ // `hostedBy`, which is handed the frond sources and cannot see the rest of the app. The
93
+ // condition is the KEYS, not the count: a scan that found nothing is an ordinary answer.
94
+ if (!options.fronds && !options.scan && !options.auth && brought.length === 0) {
95
+ throw new Error('createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
96
+ + 'Neither was given, and nothing else declares entities of its own.\n'
97
+ + ' createApp({ fronds: [blog] })\n'
98
+ + ' createApp({ scan: await scanProject(root) })');
99
+ }
100
+ const { under, refused } = nestingOf(options.under, fronds, options.remotes, options.narrowed);
101
+ const refusal = refusalOf(refused, 'thing(s) the frond tree does not allow');
102
+ if (refusal)
103
+ throw refusal;
104
+ for (const frond of fronds) {
105
+ const parent = under.get(frond.name);
106
+ if (parent !== undefined)
107
+ frond.extends = parent;
108
+ }
109
+ const ordered = parentsFirst(fronds, under);
110
+ const operationModel = resolveEffectiveOperations(ordered, {
111
+ diagnostics,
112
+ remotes: options.remotes,
113
+ adapters: options.adapters,
114
+ });
115
+ const scanMs = (performance.now() - scanStart).toFixed(0);
116
+ log.info(`read ${ordered.length} frond(s) in ${scanMs}ms`
117
+ + (diagnostics.length ? ` — ${diagnostics.length} thing(s) the scan could not do` : ''));
118
+ /** Say what could not be read, at the one line everyone already watches. */
119
+ for (const d of diagnostics.filter((one) => one.severity === 'blocking'))
120
+ log.error(`[${d.code}] ${d.message}`, d.cause);
121
+ for (const d of diagnostics)
122
+ if (d.severity === 'warning')
123
+ log.warn(`[${d.code}] ${d.message}`);
124
+ /** An ambiguous convention is not a partial scan. */
125
+ const unresolved = refusalOf(operationModel.resolutionDiagnostics, 'unresolved operation contract(s)');
126
+ if (unresolved)
127
+ throw unresolved;
128
+ return { fronds: ordered, operationModel };
129
+ }
130
+ /**
131
+ * What core answers under `rpc`, in every process, whatever fronds it carries.
132
+ *
133
+ * All four read the STORAGE and never a facade: a facade answers what its handler chose to show,
134
+ * so `PostHandler.list` hiding drafts would hide exactly the row the question exists to find.
135
+ */
136
+ function serveCoreRpc(app, hosting, storageFor) {
137
+ app.serveRpc('discover', (_invocation, surface) => identityCardOf(app, surface));
138
+ app.serveRpc('holds', async (invocation) => {
139
+ const named = String(invocation.params.entity);
140
+ const keys = (invocation.params.keys ?? []);
141
+ const rows = storageFor(named);
142
+ if (!rows)
143
+ return { missing: [] };
144
+ const found = await rows.findByKeys(keys.map(String));
145
+ return { missing: keys.filter((key) => !found.has(String(key))) };
146
+ });
147
+ app.serveRpc('dependents', (invocation) => dependentsOf(String(invocation.params.entity), hosting));
148
+ app.serveRpc('release', async (invocation) => {
149
+ await releaseRow(String(invocation.params.entity), invocation.params.key, releasing(hosting), (invocation.params.visited ?? []));
150
+ return { released: true };
151
+ });
152
+ }
153
+ /**
154
+ * What no single frond can see about a `ref()`, so it is said once every frond is installed.
155
+ *
156
+ * A target belongs to another frond, and a journal arrives with an extension, which rises last.
157
+ */
158
+ function warnAboutRelations(relations, hosting, log) {
159
+ const unfinished = unfinishable(hosting);
160
+ if (unfinished.length > 0) {
161
+ log.warn(`[relations] ${unfinished.join(', ')} — declared, and this process keeps nothing on `
162
+ + 'restart: a release interrupted here is not resumed. Install @fougere/workflow, or '
163
+ + 'expect to finish one by hand.');
164
+ }
165
+ const unheld = unheldAmong(relations, hosting);
166
+ if (unheld.length > 0) {
167
+ log.warn(`[relations] ${unheld.join(', ')} — declared, and nothing in this process holds them: `
168
+ + 'no foreign key, and the target answers elsewhere. A row may name one that is gone.');
169
+ }
170
+ }
171
+ /** The process that serves an entity, asked the three questions a release travels on. */
172
+ function peerBehind(entity, router) {
173
+ const ask = async (op, params) => (await router.route(entity)).transport({ entity: RPC_ENTITY, op }, { ...Invocation.empty, params: params });
174
+ return {
175
+ dependents: async (named) => await ask('dependents', { entity: named }),
176
+ missing: async (named, keys) => (await ask('holds', { entity: named, keys })).missing,
177
+ release: async (named, key) => { await ask('release', { entity: named, key }); },
178
+ };
179
+ }
180
+ /** Close the door, and answer once the calls already running are done. */
181
+ async function drainCalls(inflight, timeoutMs) {
182
+ inflight.close();
183
+ if (timeoutMs === undefined)
184
+ return inflight.whenIdle();
185
+ let timer;
186
+ await Promise.race([
187
+ inflight.whenIdle().then(() => clearTimeout(timer)),
188
+ new Promise((_, reject) => {
189
+ timer = setTimeout(() => reject(new Error(`[drain] ${inflight.count} call(s) still running after ${timeoutMs}ms`)), timeoutMs);
190
+ }),
191
+ ]);
192
+ }
193
+ function dispatching(options, log, fronds, journalOf) {
194
+ const inflight = new InFlight();
195
+ const globalMiddlewares = [];
196
+ const scopedMiddlewares = new Map();
197
+ const routeRegistry = new RouteRegistry();
198
+ const dispatchLifecycle = new DispatchLifecycle(options.dispatchObservers, (error, event) => log.error(`[dispatch-observer] ${event.stage} ${event.call.address.toString()}`, error));
199
+ return {
200
+ inflight,
201
+ routeRegistry,
202
+ dispatchLifecycle,
203
+ // Resolved at the call and never here: a journal is a provider of a brought frond, and the
204
+ // extension that registers it rises long after this line.
205
+ dispatcher: new Dispatcher(routeRegistry, inflight, dispatchLifecycle, undefined, journalOf),
206
+ localDispatcher: new Dispatcher(routeRegistry, inflight, dispatchLifecycle, new LocalRoutePolicy((surface) => fronds.servedNames(surface)), journalOf),
207
+ getMiddlewares: (entity) => [...globalMiddlewares, ...(scopedMiddlewares.get(entity) ?? [])],
208
+ use(middleware, entity) {
209
+ if (entity === undefined) {
210
+ globalMiddlewares.push(middleware);
211
+ return;
212
+ }
213
+ scopedMiddlewares.set(entity, [...(scopedMiddlewares.get(entity) ?? []), middleware]);
214
+ },
215
+ };
216
+ }
217
+ /** Every entity of every frond, who holds it, and the door a fact goes through. */
218
+ function whatTheAppKnows(fronds, container, options, refused) {
219
+ // By name, so a fact can be validated where it LANDS and a `reads:` clause can name a
220
+ // neighbour's entity.
221
+ const entityByName = fronds.schemas();
222
+ // The line is core's, so its SHAPE is too: a destination that declares only a handler would
223
+ // otherwise be handed a line with no `at` — the announcement stamps `created()` off the shape,
224
+ // and the strict judge refuses what it did not stamp. Measured on `demos/observability`, where
225
+ // the ring held 11 calls and 0 lines.
226
+ if (!entityByName.has(LOG_LINE))
227
+ entityByName.set(LOG_LINE, LogLine);
228
+ return {
229
+ entityByName,
230
+ // What turns "a member is remote" into a refusal that names the FROND rather than the
231
+ // entity, since `remotes:` is declared per frond.
232
+ frondOf: new Map(fronds.flatMap((one) => one.entities.map((entity) => [entity.name, one.name]))),
233
+ emissions: new Emissions(fronds, entityByName, container,
234
+ // No carry: this is what CARRIES a fact, and a line about carrying one comes back here.
235
+ new Logger('boot:app'), refused, options.onEmit),
236
+ };
237
+ }
238
+ function hostingFor({ fronds, container, options, declaredRemotes, remoteRouter, entityByName, frondOf, storageOf }) {
239
+ // What carries a release writes none: an instrumentation frond's own rows are kept while a
240
+ // release happens, and journalling them would begin one inside the one being written down.
241
+ const carriesRelease = new Set(fronds.filter((one) => one.brought).flatMap((one) => one.entities.map((entity) => entity.name)));
242
+ /**
243
+ * The journal a package registered, found where its own frond put it — a provider lands in its
244
+ * frond's scope, and the app container sees none of them. Resolved per call and never at boot:
245
+ * the package that registers it rises in the ascent, long after this answer is built.
246
+ */
247
+ const journalOf = () => {
248
+ for (const frond of fronds) {
249
+ if (!frond.brought)
250
+ continue;
251
+ const scope = container.resolve(`frond:${frond.name}`);
252
+ if (scope.has(JOURNAL))
253
+ return scope.resolve(JOURNAL);
254
+ }
255
+ return undefined;
256
+ };
257
+ const hosting = {
258
+ hostedHere: (entity) => !(options.remotes && (frondOf.get(entity) ?? '') in options.remotes),
259
+ sourceOf: (name) => options.sourceOf?.(name) ?? 'db',
260
+ enforces: (source, constraint) => options.enforces?.(source, constraint) ?? false,
261
+ storageOf,
262
+ entities: () => entityByName,
263
+ journal: (entity) => (carriesRelease.has(entity) ? undefined : journalOf()),
264
+ // Built from `remotes:` itself and not from the router: the router indexes by ENTITY, read
265
+ // off a card, while the question here is asked of a PROCESS about rows it may be alone in
266
+ // knowing about. Nothing is cached — a peer that was down at boot answers the next call.
267
+ peers: () => (options.remoteTransport
268
+ ? declaredRemotes.map(([, url]) => peerOver(options.remoteTransport(url)))
269
+ : []),
270
+ peerOf: (entity) => (remoteRouter ? peerBehind(entity, remoteRouter) : undefined),
271
+ };
272
+ return { hosting, journalOf };
273
+ }
274
+ /**
275
+ * What an app answers about what it serves — the membership rule and its neighbours.
276
+ *
277
+ * `facadeFor` is THE rule, stated once: every projection reads it and nothing else.
278
+ */
279
+ function readings({ container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log }) {
280
+ /** Stop taking calls, and resolve once the ones already running are done. */
281
+ const resolve = (name) => {
282
+ try {
283
+ return container.resolve(name);
284
+ }
285
+ catch (err) {
286
+ if (name.endsWith('Handler') && !name.includes(':') && !remoteRouter) {
287
+ throw new Error(notLoaded(name.replace(/Handler$/, '')));
288
+ }
289
+ throw err;
290
+ }
291
+ };
292
+ const schemaFor = async (entity) => {
293
+ const found = fronds.entity(entity);
294
+ if (found)
295
+ return found.entityClass;
296
+ if (remoteRouter) {
297
+ const route = await remoteRouter.route(entity);
298
+ // A remote facade that stores nothing publishes ops and no shape. Saying so beats
299
+ // handing back an empty schema, which would validate every input it was given.
300
+ if (!route.schema) {
301
+ throw new Error(`'${entity}' is served by frond '${route.frond}' but stores no rows, so it has no schema. `
302
+ + `Call its operations through the façade instead.`);
303
+ }
304
+ return route.schema;
305
+ }
306
+ throw new Error(notLoaded(entity));
307
+ };
308
+ const facadeAt = (key, topology) => {
309
+ try {
310
+ return topology
311
+ ? resolve(key)
312
+ : container.resolve(key);
313
+ }
314
+ catch {
315
+ return undefined;
316
+ }
317
+ };
318
+ /** Said once per pair, so a facade that registers in a loop says it once. */
319
+ const saidAbsent = new Set();
320
+ /**
321
+ * A surface is declared in the frond that serves it. When that frond runs in another
322
+ * process, this one never asked for its facades, and answering 'no' is the only thing a
323
+ * synchronous rule can do — so it says so rather than registering nothing in silence.
324
+ */
325
+ const sayNoSurfaceAcross = (entity, surface) => {
326
+ if (!remoteRouter || saidAbsent.has(`${surface}:${entity}`))
327
+ return;
328
+ saidAbsent.add(`${surface}:${entity}`);
329
+ log.warn(`surface '${surface}' serves nothing for '${entity}' — the frond that declares it runs `
330
+ + 'elsewhere, and a remote is asked for its facades at the first call, not at boot. '
331
+ + 'The default facade answers.');
332
+ };
333
+ /** THE membership rule, stated once — every projection reads this and nothing else. */
334
+ const facadeFor = (entity, surface) => {
335
+ if (!surface)
336
+ return facadeAt(facadeKeyOf(entity), true);
337
+ const own = facadeAt(facadeKeyOf(entity, surface), false);
338
+ const owner = fronds.owner(entity);
339
+ if (!owner) {
340
+ sayNoSurfaceAcross(entity, surface);
341
+ return own;
342
+ }
343
+ const declared = owner.surfaces?.[surface];
344
+ if (!declared)
345
+ return own;
346
+ if (!declared.some((n) => n.toLowerCase() === entity.toLowerCase()))
347
+ return undefined;
348
+ if (own)
349
+ return own;
350
+ const fallback = facadeAt(facadeKeyOf(entity), false);
351
+ return fallback
352
+ ? facadeOperations(localDispatcher, entity, routeRegistry.operationNames(entity, surface), surface)
353
+ : undefined;
354
+ };
355
+ /** The terms beside a facade, with the exact same named-surface fallback rule. */
356
+ const operationsFor = (entity, surface) => {
357
+ if (!surface)
358
+ return effectiveByKey.get(facadeKeyOf(entity));
359
+ const own = effectiveByKey.get(facadeKeyOf(entity, surface));
360
+ const declared = fronds.owner(entity)?.surfaces?.[surface];
361
+ if (!declared)
362
+ return own;
363
+ return declared.some((name) => name.toLowerCase() === entity.toLowerCase())
364
+ ? (own ?? effectiveByKey.get(facadeKeyOf(entity)))
365
+ : undefined;
366
+ };
367
+ /** The presenter of an entity, resolved through its owning frond's scope. */
368
+ const presenterFor = (entity) => {
369
+ const owner = fronds.owner(entity);
370
+ if (!owner)
371
+ return undefined;
372
+ try {
373
+ return container.resolve(`frond:${owner.name}`).resolve(presenterKeyOf(entity));
374
+ }
375
+ catch {
376
+ return undefined;
377
+ }
378
+ };
379
+ return { resolve, schemaFor, facadeFor, operationsFor, presenterFor };
380
+ }
381
+ /**
382
+ * Built once from the lazy `AuthConfig` a provider factory produced (`betterAuth({…})` in
383
+ * fougere.config.ts). The provider receives our db and storage factory, so every auth write
384
+ * flows through `Storage` like any other.
385
+ */
386
+ async function authFor(options, log) {
387
+ if (!options.auth)
388
+ return undefined;
389
+ if (!options.storageFactory) {
390
+ throw new Error('createApp: `auth` is set but `storageFactory` is missing — auth providers need it to back their adapter. Pass one through CreateAppOptions.storageFactory.');
391
+ }
392
+ if (options.db === undefined) {
393
+ throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
394
+ }
395
+ log.info('initializing auth runtime');
396
+ const runtime = await options.auth.create({ db: options.db, storageFactory: options.storageFactory });
397
+ log.info(`auth ready — mounted at ${runtime.basePath}`);
398
+ return runtime;
399
+ }
66
400
  /** Bootstrap a fougere application. */
67
401
  export async function createApp(options) {
68
- const container = options.createContainer();
402
+ const container = (options.createContainer ?? createContainer)();
69
403
  // Held out here, and not where the ascent reads it, because releasing needs it and
70
404
  // releasing has to work from the first line the boot takes something.
71
405
  // The conventional ascent, ordered here: tables, then rows, then whatever the host took
@@ -124,64 +458,8 @@ export async function createApp(options) {
124
458
  container.registerValue('Logger', new Logger('app', { carry }));
125
459
  container.register('Config', Config, { lifetime: 'singleton' });
126
460
  log.debug('builtins registered (Logger, Config)');
127
- // What this app hosts is HANDED IN — stated, scanned, or both (`hostedBy`). Producing
128
- // it may read a disk; consuming it never does, which is the whole reason this file names
129
- // no builtin and a Worker can run what it builds.
130
- const scanStart = performance.now();
131
- // An extension's fronds sit beside the app's own: it is installed like any other, and
132
- // its handlers resolve at call time — by which point the extension's `up` has put what
133
- // they ask for in the container.
134
- const brought = (options.extensions ?? [])
135
- .flatMap((extension) => extension?.fronds ?? [])
136
- .map((frond) => ({ ...frond, brought: true }));
137
- const { fronds, diagnostics } = await hostedBy(brought.length > 0 ? { ...options, fronds: [...(options.fronds ?? []), ...brought] } : options);
138
- // An app that states nothing AND scans nothing is a mistake — unless something else it
139
- // declares brings its own entities, which an auth provider does. Refused here and not in
140
- // `hostedBy`, which is handed the frond sources and cannot see the rest of the app. The
141
- // condition is the KEYS, not the count: a scan that found nothing is an ordinary answer.
142
- if (!options.fronds && !options.scan && !options.auth && brought.length === 0) {
143
- throw new Error('createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
144
- + 'Neither was given, and nothing else declares entities of its own.\n'
145
- + ' createApp({ fronds: [blog] })\n'
146
- + ' createApp({ scan: await scanProject(root) })');
147
- }
148
- const operationModel = resolveEffectiveOperations(fronds, {
149
- diagnostics,
150
- remotes: options.remotes,
151
- adapters: options.adapters,
152
- });
153
- const scanMs = (performance.now() - scanStart).toFixed(0);
154
- const blocking = diagnostics.filter((d) => d.severity === 'blocking');
155
- log.info(`read ${fronds.length} frond(s) in ${scanMs}ms`
156
- + (diagnostics.length ? ` — ${diagnostics.length} thing(s) the scan could not do` : ''));
157
- /** Say what could not be read, at the one line everyone already watches. */
158
- for (const d of blocking)
159
- log.error(`[${d.code}] ${d.message}`, d.cause);
160
- for (const d of diagnostics)
161
- if (d.severity === 'warning')
162
- log.warn(`[${d.code}] ${d.message}`);
163
- /** An ambiguous convention is not a partial scan. */
164
- const unresolved = refusalOf(operationModel.resolutionDiagnostics, 'unresolved operation contract(s)');
165
- if (unresolved)
166
- throw unresolved;
167
- // Auth runtime — built once from the lazy AuthConfig produced by a provider factory
168
- // (e.g. betterAuth({...})) in fougere.config.ts. The provider receives our db +
169
- // storageFactory so all auth writes flow through Storage.
170
- let authRuntime;
171
- if (options.auth) {
172
- if (!options.storageFactory) {
173
- throw new Error('createApp: `auth` is set but `storageFactory` is missing — auth providers need it to back their adapter. Pass one through CreateAppOptions.storageFactory.');
174
- }
175
- if (options.db === undefined) {
176
- throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
177
- }
178
- log.info('initializing auth runtime');
179
- authRuntime = await options.auth.create({
180
- db: options.db,
181
- storageFactory: options.storageFactory,
182
- });
183
- log.info(`auth ready — mounted at ${authRuntime.basePath}`);
184
- }
461
+ const { fronds, operationModel } = await readFronds(options, log);
462
+ const authRuntime = await authFor(options, log);
185
463
  // Remote routing — validated at boot: declaring remotes without a transport is a config error.
186
464
  // A remote declaration wins over local presence: `remotes: { blog: url }` IS
187
465
  // the topology statement — the frond's code may sit in fronds/**, it runs elsewhere.
@@ -192,62 +470,41 @@ export async function createApp(options) {
192
470
  const remoteRouter = declaredRemotes.length > 0 && options.remoteTransport
193
471
  ? createRemoteRouter(Object.fromEntries(declaredRemotes), options.remoteTransport)
194
472
  : undefined;
195
- // What is running on this app — counted at the one facade every caller goes through,
196
- // so releasing it can wait for the work instead of pulling the floor out.
197
- const inflight = new InFlight();
198
- // Middleware storage — read at call time, not at boot time
199
- const globalMiddlewares = [];
200
- const scopedMiddlewares = new Map();
201
- /** What this app took on beyond its fronds. Its `up` is the last thing the boot does. */
202
- const routeRegistry = new RouteRegistry();
203
- const dispatchLifecycle = new DispatchLifecycle(options.dispatchObservers, (error, event) => log.error(`[dispatch-observer] ${event.stage} ${event.call.address.toString()}`, error));
204
- const dispatcher = new Dispatcher(routeRegistry, inflight, dispatchLifecycle);
205
- const localDispatcher = new Dispatcher(routeRegistry, inflight, dispatchLifecycle, new LocalRoutePolicy((surface) => fronds.servedNames(surface)));
206
- function getMiddlewares(entity) {
207
- const scoped = scopedMiddlewares.get(entity) ?? [];
208
- return [...globalMiddlewares, ...scoped];
209
- }
210
- /**
211
- * The one place a middleware is taken on. `App.use` is its late form, and a frond's
212
- * `middlewares/` its early one — the app does not exist yet while fronds install.
213
- */
214
- function use(middleware, entity) {
215
- if (entity === undefined) {
216
- globalMiddlewares.push(middleware);
217
- return;
218
- }
219
- const scoped = scopedMiddlewares.get(entity) ?? [];
220
- scoped.push(middleware);
221
- scopedMiddlewares.set(entity, scoped);
222
- }
473
+ const { inflight, routeRegistry, dispatchLifecycle, dispatcher, localDispatcher, getMiddlewares, use } = dispatching(options, log, fronds, () => journalOf());
223
474
  /** What every check of this boot writes into — refused together, once they have all run. */
224
475
  const refused = [];
476
+ const relations = [];
225
477
  keyClaims(fronds, options.remotes, refused);
226
478
  // Said before anything is installed: a key claimed twice makes every later error worse —
227
479
  // the route registry collides first, and names a route instead of the two fronds.
228
480
  const claimed = refusalOf(refused, 'declaration(s) that do not hold');
229
481
  if (claimed)
230
482
  throw claimed;
231
- // Every entity of every frond, by name — so a fact can be validated where it LANDS, and
232
- // so a `reads:` clause can name a neighbour's.
233
- const entityByName = fronds.schemas();
234
- // The line is core's, so its SHAPE is too: a destination that declares only a handler
235
- // would otherwise be handed a line with no `at` — the announcement stamps `created()`
236
- // off the shape, and the strict judge refuses what it did not stamp. Measured on
237
- // `demos/observability`, where the ring held 11 calls and 0 lines.
238
- if (!entityByName.has(LOG_LINE))
239
- entityByName.set(LOG_LINE, LogLine);
240
- // Which frond holds an entity — what turns "a member is remote" into a refusal that
241
- // names the frond rather than the entity, since `remotes:` is declared per frond.
242
- const frondOf = new Map(fronds.flatMap((f) => f.entities.map((e) => [e.name, f.name])));
243
- // Its own writer, which does NOT announce: this is what carries a fact, and a line
244
- // about carrying one would come back here. See `LoggerOptions.carries`.
245
- const emissions = new Emissions(fronds, entityByName, container,
246
- // No carry: this is what CARRIES a fact, and a line about carrying one comes back.
247
- new Logger('boot:app'), refused, options.onEmit);
483
+ const { entityByName, frondOf, emissions } = whatTheAppKnows(fronds, container, options, refused);
248
484
  /** Canonical operation tables, indexed by the same audience key as their facades. */
249
485
  const effectiveByKey = new Map();
250
486
  const contractsOf = (operations) => new Map([...operations].map(([name, operation]) => [name, operation]));
487
+ /**
488
+ * The storage an entity is backed by — the dual of `facadeFor`, which serves its client-facing
489
+ * facade.
490
+ */
491
+ const storageFor = (entity) => {
492
+ const owner = fronds.owner(entity);
493
+ if (!owner)
494
+ return undefined;
495
+ const key = storageKeyOf(entity);
496
+ try {
497
+ return container.resolve(`frond:${owner.name}`).resolve(key);
498
+ }
499
+ catch {
500
+ return undefined;
501
+ }
502
+ };
503
+ // What carries a release writes none: an instrumentation frond's own rows are kept while a
504
+ // release happens, and journalling them would begin one inside the one being written down.
505
+ const { hosting, journalOf } = hostingFor({
506
+ fronds, container, options, declaredRemotes, remoteRouter, entityByName, frondOf, storageOf: storageFor,
507
+ });
251
508
  // Every port an implementation was bound to, so a `ports:` entry that named none
252
509
  // can say so rather than look obeyed.
253
510
  const boundPorts = new Set();
@@ -256,11 +513,12 @@ export async function createApp(options) {
256
513
  // table, one emission list — so what a frond serves is there for the next one to find.
257
514
  const assembly = {
258
515
  container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
259
- boundPorts, refused, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
260
- log, options,
516
+ boundPorts, refused, relations, hosting, operationModel, entityByName, frondOf, contractsOf,
517
+ getMiddlewares, use, middlewaresOf: new Map(), seamsOf: new Map(), log, options,
261
518
  };
262
519
  for (const frond of fronds)
263
520
  await installFrond(frond, assembly);
521
+ warnAboutRelations(relations, hosting, log);
264
522
  // Every check of the install, said at once — a boot that stops at the first makes the
265
523
  // next one visible only after a fix and a restart.
266
524
  const bootRefusal = refusalOf(refused, 'declaration(s) that do not hold');
@@ -299,93 +557,6 @@ export async function createApp(options) {
299
557
  // 'Product' and every by-type dependency on a remote handler answered NOT_FOUND.
300
558
  return facadeOperations(dispatcher, lowerFirst(name.replace(/Handler$/, '')));
301
559
  });
302
- /** Stop taking calls, and resolve once the ones already running are done. */
303
- const drain = async (timeoutMs) => {
304
- inflight.close();
305
- if (timeoutMs === undefined)
306
- return inflight.whenIdle();
307
- let timer;
308
- await Promise.race([
309
- inflight.whenIdle().then(() => clearTimeout(timer)),
310
- new Promise((_, reject) => {
311
- timer = setTimeout(() => reject(new Error(`[drain] ${inflight.count} call(s) still running after ${timeoutMs}ms`)), timeoutMs);
312
- }),
313
- ]);
314
- };
315
- const resolve = (name) => {
316
- try {
317
- return container.resolve(name);
318
- }
319
- catch (err) {
320
- if (name.endsWith('Handler') && !name.includes(':') && !remoteRouter) {
321
- throw new Error(notLoaded(name.replace(/Handler$/, '')));
322
- }
323
- throw err;
324
- }
325
- };
326
- const schemaFor = async (entity) => {
327
- const found = fronds.entity(entity);
328
- if (found)
329
- return found.entityClass;
330
- if (remoteRouter) {
331
- const route = await remoteRouter.route(entity);
332
- // A remote facade that stores nothing publishes ops and no shape. Saying so beats
333
- // handing back an empty schema, which would validate every input it was given.
334
- if (!route.schema) {
335
- throw new Error(`'${entity}' is served by frond '${route.frond}' but stores no rows, so it has no schema. `
336
- + `Call its operations through the façade instead.`);
337
- }
338
- return route.schema;
339
- }
340
- throw new Error(notLoaded(entity));
341
- };
342
- const facadeAt = (key, topology) => {
343
- try {
344
- return topology
345
- ? resolve(key)
346
- : container.resolve(key);
347
- }
348
- catch {
349
- return undefined;
350
- }
351
- };
352
- /** Said once per pair, so a facade that registers in a loop says it once. */
353
- const saidAbsent = new Set();
354
- /**
355
- * A surface is declared in the frond that serves it. When that frond runs in another
356
- * process, this one never asked for its facades, and answering 'no' is the only thing a
357
- * synchronous rule can do — so it says so rather than registering nothing in silence.
358
- */
359
- const sayNoSurfaceAcross = (entity, surface) => {
360
- if (!remoteRouter || saidAbsent.has(`${surface}:${entity}`))
361
- return;
362
- saidAbsent.add(`${surface}:${entity}`);
363
- log.warn(`surface '${surface}' serves nothing for '${entity}' — the frond that declares it runs `
364
- + 'elsewhere, and a remote is asked for its facades at the first call, not at boot. '
365
- + 'The default facade answers.');
366
- };
367
- /** THE membership rule, stated once — every projection reads this and nothing else. */
368
- const facadeFor = (entity, surface) => {
369
- if (!surface)
370
- return facadeAt(facadeKeyOf(entity), true);
371
- const own = facadeAt(facadeKeyOf(entity, surface), false);
372
- const owner = fronds.owner(entity);
373
- if (!owner) {
374
- sayNoSurfaceAcross(entity, surface);
375
- return own;
376
- }
377
- const declared = owner.surfaces?.[surface];
378
- if (!declared)
379
- return own;
380
- if (!declared.some((n) => n.toLowerCase() === entity.toLowerCase()))
381
- return undefined;
382
- if (own)
383
- return own;
384
- const fallback = facadeAt(facadeKeyOf(entity), false);
385
- return fallback
386
- ? facadeOperations(localDispatcher, entity, routeRegistry.operationNames(entity, surface), surface)
387
- : undefined;
388
- };
389
560
  if (remoteRouter) {
390
561
  const remoteFacades = new Map();
391
562
  routeRegistry.addResolver(remoteRoutes((entity) => {
@@ -397,46 +568,9 @@ export async function createApp(options) {
397
568
  return facade;
398
569
  }));
399
570
  }
400
- /** The terms beside a facade, with the exact same named-surface fallback rule. */
401
- const operationsFor = (entity, surface) => {
402
- if (!surface)
403
- return effectiveByKey.get(facadeKeyOf(entity));
404
- const own = effectiveByKey.get(facadeKeyOf(entity, surface));
405
- const declared = fronds.owner(entity)?.surfaces?.[surface];
406
- if (!declared)
407
- return own;
408
- return declared.some((name) => name.toLowerCase() === entity.toLowerCase())
409
- ? (own ?? effectiveByKey.get(facadeKeyOf(entity)))
410
- : undefined;
411
- };
412
- /**
413
- * The storage an entity is backed by — the dual of `facadeFor`, which serves its client-facing
414
- * facade.
415
- */
416
- const storageFor = (entity) => {
417
- const owner = fronds.owner(entity);
418
- if (!owner)
419
- return undefined;
420
- const key = storageKeyOf(entity);
421
- try {
422
- return container.resolve(`frond:${owner.name}`).resolve(key);
423
- }
424
- catch {
425
- return undefined;
426
- }
427
- };
428
- /** The presenter of an entity, resolved through its owning frond's scope. */
429
- const presenterFor = (entity) => {
430
- const owner = fronds.owner(entity);
431
- if (!owner)
432
- return undefined;
433
- try {
434
- return container.resolve(`frond:${owner.name}`).resolve(presenterKeyOf(entity));
435
- }
436
- catch {
437
- return undefined;
438
- }
439
- };
571
+ const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
572
+ container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log,
573
+ });
440
574
  const app = {
441
575
  container,
442
576
  fronds,
@@ -458,7 +592,7 @@ export async function createApp(options) {
458
592
  storageFor,
459
593
  presenterFor,
460
594
  dispose: release,
461
- drain,
595
+ drain: (timeoutMs) => drainCalls(inflight, timeoutMs),
462
596
  inFlight: () => inflight.count,
463
597
  [Symbol.asyncDispose]: release,
464
598
  serveRpc(op, answer) {
@@ -484,7 +618,10 @@ export async function createApp(options) {
484
618
  };
485
619
  // The card is an rpc op like any other, so one registry answers and one refusal names
486
620
  // what is served. A package's op is declared the same way, from outside.
487
- app.serveRpc('discover', (_invocation, surface) => identityCardOf(app, surface));
621
+ // What a release is, resolved once and registered: a package that drives one asks for it
622
+ // by type, the way every other dependency is asked for.
623
+ container.registerValue('Releasing', releasing(hosting));
624
+ serveCoreRpc(app, hosting, storageFor);
488
625
  // The last thing the boot does, and the first thing a release undoes. An extension may
489
626
  // await here — which is what a provider needing to OPEN something could never do.
490
627
  built = app;