@fougere/core 0.12.0-alpha.0 → 0.12.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/DomainCluster.d.ts.map +1 -1
- package/dist/DomainCluster.js +1 -1
- package/dist/DomainCluster.js.map +1 -1
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +20 -1
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/Source.js +2 -2
- package/dist/Source.js.map +1 -1
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/ambient.als.d.ts.map +1 -1
- package/dist/boot/ambient.als.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/arity.d.ts +13 -0
- package/dist/boot/arity.d.ts.map +1 -0
- package/dist/boot/arity.js +33 -0
- package/dist/boot/arity.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +3 -2
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +8 -2
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/relations.d.ts +10 -0
- package/dist/boot/relations.d.ts.map +1 -1
- package/dist/boot/relations.js +35 -2
- package/dist/boot/relations.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +8 -2
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.js +1 -1
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +6 -0
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js +19 -0
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/builtin/Carry.d.ts.map +1 -1
- package/dist/builtin/Carry.js.map +1 -1
- package/dist/builtin/Logger.d.ts.map +1 -1
- package/dist/builtin/Logger.js +3 -1
- package/dist/builtin/Logger.js.map +1 -1
- package/dist/builtin/config.js.map +1 -1
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/crypto/node.d.ts.map +1 -1
- package/dist/crypto/node.js.map +1 -1
- package/dist/crypto/webcrypto.d.ts.map +1 -1
- package/dist/crypto/webcrypto.js.map +1 -1
- package/dist/descriptor/Fronds.d.ts.map +1 -1
- package/dist/descriptor/Fronds.js.map +1 -1
- package/dist/dispatch/ArrayResult.d.ts.map +1 -1
- package/dist/dispatch/ArrayResult.js.map +1 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +11 -3
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
- package/dist/dispatch/OutputView.d.ts.map +1 -1
- package/dist/dispatch/OutputView.js +7 -0
- package/dist/dispatch/OutputView.js.map +1 -1
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
- package/dist/dispatch/PresenterExecutor.js +7 -0
- package/dist/dispatch/PresenterExecutor.js.map +1 -1
- package/dist/dispatch/Received.d.ts +3 -0
- package/dist/dispatch/Received.d.ts.map +1 -0
- package/dist/dispatch/Received.js +2 -0
- package/dist/dispatch/Received.js.map +1 -0
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
- package/dist/dispatch/RouteRegistry.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +4 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/dispatch/decoded.d.ts +19 -0
- package/dist/dispatch/decoded.d.ts.map +1 -0
- package/dist/dispatch/decoded.js +36 -0
- package/dist/dispatch/decoded.js.map +1 -0
- package/dist/entry/facade.d.ts +9 -2
- package/dist/entry/facade.d.ts.map +1 -1
- package/dist/entry/facade.js +16 -7
- package/dist/entry/facade.js.map +1 -1
- package/dist/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js +3 -2
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/CrudOps.d.ts +2 -2
- package/dist/prefab/CrudOps.d.ts.map +1 -1
- package/dist/prefab/MirrorConstructor.js +1 -1
- package/dist/prefab/MirrorConstructor.js.map +1 -1
- package/dist/prefab/Refreshed.d.ts +7 -2
- package/dist/prefab/Refreshed.d.ts.map +1 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +7 -11
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts +10 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/AppNext.d.ts +2 -1
- package/dist/wire/AppNext.d.ts.map +1 -1
- package/dist/wire/Facade.d.ts +14 -2
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/Page.d.ts +32 -0
- package/dist/wire/Page.d.ts.map +1 -0
- package/dist/wire/Page.js +32 -0
- package/dist/wire/Page.js.map +1 -0
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/Signature.d.ts +7 -0
- package/dist/wire/Signature.d.ts.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/IdentityCard.d.ts.map +1 -1
- package/dist/wire/card/IdentityCard.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js.map +1 -1
- package/dist/wire/http-error.d.ts.map +1 -1
- package/dist/wire/http-error.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +5 -5
- package/src/CallIdentity.ts +6 -0
- package/src/DomainCluster.ts +2 -1
- package/src/EffectiveOperationModel.ts +38 -1
- package/src/FougereConfig.ts +3 -0
- package/src/FrondConfig.ts +2 -0
- package/src/Source.ts +2 -2
- package/src/boot/AppLifecycle.ts +1 -0
- package/src/boot/Emissions.ts +4 -0
- package/src/boot/ambient.als.ts +2 -0
- package/src/boot/apply.ts +1 -0
- package/src/boot/arity.ts +42 -0
- package/src/boot/bootstrap.ts +9 -2
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +8 -1
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +39 -2
- package/src/boot/remote.ts +10 -2
- package/src/boot/seed.ts +1 -1
- package/src/boot/statement-drift.ts +25 -0
- package/src/boot/together.ts +3 -0
- package/src/builtin/Carry.ts +1 -0
- package/src/builtin/Logger.ts +9 -1
- package/src/builtin/config.ts +1 -0
- package/src/contract.ts +3 -0
- package/src/crypto/encoding.ts +4 -0
- package/src/crypto/node.ts +2 -0
- package/src/crypto/webcrypto.ts +2 -0
- package/src/descriptor/Fronds.ts +2 -0
- package/src/dispatch/ArrayResult.ts +1 -0
- package/src/dispatch/Dispatcher.ts +1 -0
- package/src/dispatch/HandlerFacade.ts +16 -4
- package/src/dispatch/InFlight.ts +2 -0
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/OutputView.ts +7 -0
- package/src/dispatch/PresenterExecutor.ts +7 -0
- package/src/dispatch/Received.ts +2 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/dispatch/decoded.ts +37 -0
- package/src/entry/facade.ts +21 -9
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +1 -0
- package/src/loader.ts +1 -0
- package/src/prefab/CrudConstructor.ts +4 -2
- package/src/prefab/CrudOps.ts +2 -2
- package/src/prefab/MirrorConstructor.ts +1 -1
- package/src/prefab/Refreshed.ts +7 -2
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +15 -10
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +12 -1
- package/src/wire/AppNext.ts +2 -1
- package/src/wire/Facade.ts +14 -2
- package/src/wire/FougereError.ts +3 -1
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/Page.ts +50 -0
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/Signature.ts +7 -0
- package/src/wire/binding.ts +1 -0
- package/src/wire/card/IdentityCard.ts +1 -0
- package/src/wire/drift.ts +3 -0
- package/src/wire/http-error.ts +2 -0
- package/src/wire/loggerMiddleware.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
2
|
-
import { statementDrift } from './boot/statement-drift.js';
|
|
2
|
+
import { statementDrift, unansweredOperations } from './boot/statement-drift.js';
|
|
3
3
|
import { reachedBy, servedBy } from './boot/declared.js';
|
|
4
4
|
import { computeBindingPlan, type BindingPlan } from './wire/binding.js';
|
|
5
5
|
import { targetOf } from './prefab/prefab.js';
|
|
@@ -111,12 +111,32 @@ export function resolveEffectiveOperations(
|
|
|
111
111
|
// A statement wins over the scan on purpose; saying so out loud is what keeps the
|
|
112
112
|
// win from hiding a rename. Compared here, where both readings are in hand.
|
|
113
113
|
resolutionDiagnostics.push(...statementDrift(frond, handler));
|
|
114
|
+
resolutionDiagnostics.push(...unansweredOperations(frond, handler, contracts.keys()));
|
|
114
115
|
|
|
115
116
|
for (const [name, rawContract] of contracts) {
|
|
116
117
|
const subject = `${handler.ctor.name}.${name}`;
|
|
117
118
|
const contract = normalizeBinding(rawContract, handler, frond, name, resolutionDiagnostics);
|
|
118
119
|
if (!contract) continue;
|
|
119
120
|
|
|
121
|
+
// An operation answers DATA — what JSON keeps. A DECLARED output is converted by its
|
|
122
|
+
// fields, so `created()` leaves as an ISO string and nothing is asked of it here; an
|
|
123
|
+
// undeclared one has nothing to convert it, so it must already be data. Measured
|
|
124
|
+
// 2026-09-18: `{ at: new Date(0) }` reaches a local caller as a `Date` and a remote one
|
|
125
|
+
// as a string, and an object carrying methods loses them on both sides, differently.
|
|
126
|
+
if (!contract.output && contract.signature?.notData) {
|
|
127
|
+
resolutionDiagnostics.push({
|
|
128
|
+
severity: 'blocking',
|
|
129
|
+
code: 'operation-output-not-data',
|
|
130
|
+
filePath: handler.filePath,
|
|
131
|
+
frond: frond.name,
|
|
132
|
+
subject,
|
|
133
|
+
message: `${subject}() answers ${contract.signature.notData}, which carries methods.\n`
|
|
134
|
+
+ ' An operation answers data — what JSON keeps.\n'
|
|
135
|
+
+ ' Declare the output as an entity, whose fields convert it, or answer what it produces.',
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
const override = frond.operationsOverrides?.[name];
|
|
121
141
|
const inference = inferOperationKind(name);
|
|
122
142
|
const inferredKinds = new Set<OperationKind>();
|
|
@@ -155,6 +175,7 @@ export function resolveEffectiveOperations(
|
|
|
155
175
|
const undefinable = binding.optional
|
|
156
176
|
|| param?.optional === true
|
|
157
177
|
|| param?.type.undefined === true;
|
|
178
|
+
|
|
158
179
|
return {
|
|
159
180
|
position,
|
|
160
181
|
name: param?.name ?? binding.name,
|
|
@@ -219,6 +240,7 @@ export function resolveEffectiveOperations(
|
|
|
219
240
|
collectors: parameters.flatMap((parameter) => {
|
|
220
241
|
if (parameter.binding.source.kind !== 'collector') return [];
|
|
221
242
|
const collector = collectorsByType.get(parameter.binding.source.typeName)?.[0];
|
|
243
|
+
|
|
222
244
|
return collector ? [{
|
|
223
245
|
parameter: parameter.name,
|
|
224
246
|
typeName: collector.typeName,
|
|
@@ -260,6 +282,7 @@ export function resolveEffectiveOperations(
|
|
|
260
282
|
}
|
|
261
283
|
|
|
262
284
|
const resolution = uniqueDiagnostics(resolutionDiagnostics);
|
|
285
|
+
|
|
263
286
|
return new EffectiveOperationModel(
|
|
264
287
|
operations,
|
|
265
288
|
uniqueDiagnostics([...scanDiagnostics, ...resolution]),
|
|
@@ -275,6 +298,7 @@ function groupedCollectors(collectors: readonly CollectorEntry[]): Map<string, C
|
|
|
275
298
|
sameName.push(collector);
|
|
276
299
|
grouped.set(collector.typeName, sameName);
|
|
277
300
|
}
|
|
301
|
+
|
|
278
302
|
return grouped;
|
|
279
303
|
}
|
|
280
304
|
|
|
@@ -296,6 +320,7 @@ function normalizeBinding(
|
|
|
296
320
|
subject: `${handler.ctor.name}.${name}`,
|
|
297
321
|
message: `${handler.ctor.name}.${name} declares ${params.length} parameter(s) and has no binding plan.`,
|
|
298
322
|
});
|
|
323
|
+
|
|
299
324
|
return undefined;
|
|
300
325
|
}
|
|
301
326
|
if (params.length === 0) return { ...contract, binding: contract.binding };
|
|
@@ -322,8 +347,10 @@ function normalizeBinding(
|
|
|
322
347
|
+ `${invalid.length ? `Invalid: ${invalid.map((param) => param.name).join(', ')}. ` : ''}`
|
|
323
348
|
+ `${extras.length ? `Unknown: ${extras.join(', ')}.` : ''}`,
|
|
324
349
|
});
|
|
350
|
+
|
|
325
351
|
return undefined;
|
|
326
352
|
}
|
|
353
|
+
|
|
327
354
|
return {
|
|
328
355
|
...contract,
|
|
329
356
|
binding: params.map((param) => byName.get(param.name)![0]!),
|
|
@@ -368,6 +395,7 @@ function validateProvenance(
|
|
|
368
395
|
+ `${matches.map((parameter) => `${parameter.name}: ${parameter.type ?? 'unknown'}`).join('; ')} `
|
|
369
396
|
+ 'all match the resolved input contract. Declare an explicit binding plan.',
|
|
370
397
|
});
|
|
398
|
+
|
|
371
399
|
return false;
|
|
372
400
|
}
|
|
373
401
|
if (matches.length === 1) inferredBody.add(matches[0]!);
|
|
@@ -429,6 +457,7 @@ function validateProvenance(
|
|
|
429
457
|
+ 'local collector or resolved input schema. Declare an explicit binding.',
|
|
430
458
|
});
|
|
431
459
|
}
|
|
460
|
+
|
|
432
461
|
return valid;
|
|
433
462
|
}
|
|
434
463
|
|
|
@@ -439,6 +468,7 @@ function structural(type: TypeRef | undefined): boolean {
|
|
|
439
468
|
function schemaMatches(schema: SchemaView, type: TypeRef | undefined): boolean {
|
|
440
469
|
const names = new Set([schema.name, schema.derivation?.sourceName].filter(Boolean));
|
|
441
470
|
if (type?.name && names.has(type.name)) return true;
|
|
471
|
+
|
|
442
472
|
return type?.generics?.some((generic) => generic.name && names.has(generic.name)) ?? false;
|
|
443
473
|
}
|
|
444
474
|
|
|
@@ -462,6 +492,7 @@ function implementationOf(
|
|
|
462
492
|
message: `Operation ${handler.ctor.name}.${name} names handler '${override?.handlerName}', `
|
|
463
493
|
+ `but ${candidates.length} matching handlers were found.`,
|
|
464
494
|
});
|
|
495
|
+
|
|
465
496
|
return undefined;
|
|
466
497
|
}
|
|
467
498
|
const implementation = candidates[0]!;
|
|
@@ -479,8 +510,10 @@ function implementationOf(
|
|
|
479
510
|
message: `Operation ${handler.ctor.name}.${name} resolves to `
|
|
480
511
|
+ `${implementation.ctor.name}.${method}, but that method does not exist.`,
|
|
481
512
|
});
|
|
513
|
+
|
|
482
514
|
return undefined;
|
|
483
515
|
}
|
|
516
|
+
|
|
484
517
|
return {
|
|
485
518
|
className: implementation.ctor.name,
|
|
486
519
|
address: implementation.address,
|
|
@@ -505,6 +538,7 @@ function effectiveOutput(
|
|
|
505
538
|
const target = targetOf(handler.ctor) as { name?: string } | undefined;
|
|
506
539
|
const address = target?.name ? lowerFirst(target.name) : handler.address;
|
|
507
540
|
const entity = frond.entities.find((candidate) => candidate.name === address);
|
|
541
|
+
|
|
508
542
|
return { ...(entity ? { schema: entity.entityClass } : {}), closed: false };
|
|
509
543
|
}
|
|
510
544
|
|
|
@@ -513,6 +547,7 @@ function exposedAdapters(
|
|
|
513
547
|
adapters: Record<string, boolean | undefined> | undefined,
|
|
514
548
|
): string[] {
|
|
515
549
|
if (handler.exposed === false && !handler.surface) return [];
|
|
550
|
+
|
|
516
551
|
return Object.entries(adapters ?? {})
|
|
517
552
|
.filter(([, enabled]) => enabled === true)
|
|
518
553
|
.map(([adapter]) => adapter)
|
|
@@ -521,10 +556,12 @@ function exposedAdapters(
|
|
|
521
556
|
|
|
522
557
|
function uniqueDiagnostics(diagnostics: readonly Diagnostic[]): Diagnostic[] {
|
|
523
558
|
const seen = new Set<string>();
|
|
559
|
+
|
|
524
560
|
return diagnostics.filter((diagnostic) => {
|
|
525
561
|
const key = [diagnostic.code, diagnostic.filePath, diagnostic.subject, diagnostic.message].join('\0');
|
|
526
562
|
if (seen.has(key)) return false;
|
|
527
563
|
seen.add(key);
|
|
564
|
+
|
|
528
565
|
return true;
|
|
529
566
|
});
|
|
530
567
|
}
|
package/src/FougereConfig.ts
CHANGED
|
@@ -47,9 +47,11 @@ async function loadConfigFrom(dir: string, fresh?: boolean): Promise<FougereConf
|
|
|
47
47
|
// return the config already in force. The loader owns its own cache, so it is the
|
|
48
48
|
// one told; the old module stays in memory, re-reading being for a change.
|
|
49
49
|
const mod = await loader(path, fresh ? { fresh } : undefined);
|
|
50
|
+
|
|
50
51
|
return ((mod as { default?: FougereConfig }).default ?? mod) as FougereConfig;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
|
|
53
55
|
return {};
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -92,5 +94,6 @@ function mergeGlobal(base: FougereConfig, override: Partial<FougereConfig>): Fou
|
|
|
92
94
|
export async function loadCascadedConfig(workspaceRoot: string, appRoot: string): Promise<FougereConfig> {
|
|
93
95
|
const base = await loadConfig(workspaceRoot);
|
|
94
96
|
if (resolve(workspaceRoot) === resolve(appRoot)) return base;
|
|
97
|
+
|
|
95
98
|
return mergeGlobal(base, await loadConfig(appRoot));
|
|
96
99
|
}
|
package/src/FrondConfig.ts
CHANGED
|
@@ -44,8 +44,10 @@ export async function loadFrondConfig(frondPath: string): Promise<FrondConfig |
|
|
|
44
44
|
const path = resolve(frondPath, file);
|
|
45
45
|
if (existsSync(path)) {
|
|
46
46
|
const mod = await import(path);
|
|
47
|
+
|
|
47
48
|
return mod.default ?? mod;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
|
|
50
52
|
return undefined;
|
|
51
53
|
}
|
package/src/Source.ts
CHANGED
|
@@ -7,10 +7,10 @@ import type { SourceConfig } from './SourceConfig.js';
|
|
|
7
7
|
/** Whether an entity asks for one, anywhere but on its key. */
|
|
8
8
|
export function declares(schema: SchemaView, constraint: Constraint): boolean {
|
|
9
9
|
const fields = Object.values(schema.getFields());
|
|
10
|
-
if (constraint === 'relation') return fields.some((field) => Role.of(field).isReference);
|
|
10
|
+
if (constraint === 'relation') return fields.some((field) => Role.of(field).isReference());
|
|
11
11
|
if ((schema.getUnique() ?? []).length > 0) return true;
|
|
12
12
|
|
|
13
|
-
return fields.some((field) => Role.of(field).isUnique);
|
|
13
|
+
return fields.some((field) => Role.of(field).isUnique());
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
package/src/boot/AppLifecycle.ts
CHANGED
package/src/boot/Emissions.ts
CHANGED
|
@@ -318,6 +318,7 @@ export class Emissions {
|
|
|
318
318
|
*/
|
|
319
319
|
private stamped(fact: string, raw: unknown): unknown {
|
|
320
320
|
const shape = this.shapes.get(fact);
|
|
321
|
+
|
|
321
322
|
return shape && raw !== null && typeof raw === 'object' && !Array.isArray(raw)
|
|
322
323
|
? applyCreate(shape.getFields(), raw as Record<string, unknown>)
|
|
323
324
|
: raw;
|
|
@@ -337,6 +338,7 @@ export class Emissions {
|
|
|
337
338
|
const listeners = this.subscribers.get(fact) ?? [];
|
|
338
339
|
if (listeners.length === 0) {
|
|
339
340
|
this.log.debug(`${fact} — nobody listens in this process`);
|
|
341
|
+
|
|
340
342
|
return [];
|
|
341
343
|
}
|
|
342
344
|
|
|
@@ -350,6 +352,7 @@ export class Emissions {
|
|
|
350
352
|
} catch (cause) {
|
|
351
353
|
throw new Error(`${fact} → ${facade} could not be reached`, { cause });
|
|
352
354
|
}
|
|
355
|
+
|
|
353
356
|
return handler[op]({ ...Invocation.empty, input: payload });
|
|
354
357
|
}),
|
|
355
358
|
}));
|
|
@@ -359,6 +362,7 @@ export class Emissions {
|
|
|
359
362
|
private describeRefusal(fact: string, cause: unknown): string | undefined {
|
|
360
363
|
const refusals = validationErrorsOf(cause);
|
|
361
364
|
if (!refusals?.length) return undefined;
|
|
365
|
+
|
|
362
366
|
return `refused the shape — ${refusals.map((d) => `${dotted(d.path)}: ${d.message}`).join(', ')}.`
|
|
363
367
|
+ ` If '${fact}' gained a field, this copy is older than the sender's: re-run \`fougere sync\`.`;
|
|
364
368
|
}
|
package/src/boot/ambient.als.ts
CHANGED
|
@@ -17,12 +17,14 @@ export const ambient: Ambient = {
|
|
|
17
17
|
+ `are ONE frame — declare a single Together naming every member.`,
|
|
18
18
|
));
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
return frame.run(key, fn);
|
|
21
22
|
},
|
|
22
23
|
|
|
23
24
|
beforeAnnounce(fact: string): Promise<void> {
|
|
24
25
|
const open = frame.getStore();
|
|
25
26
|
if (open === undefined) return Promise.resolve();
|
|
27
|
+
|
|
26
28
|
return Promise.reject(new Error(
|
|
27
29
|
`${fact} cannot be announced inside Together<[…]> (${open}): announcing is dispatch, `
|
|
28
30
|
+ `so subscribers and the carrier would have it while these writes can still be taken `
|
package/src/boot/apply.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Diagnostic } from '../diagnostic.js';
|
|
2
|
+
import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
|
|
3
|
+
|
|
4
|
+
interface Constructed {
|
|
5
|
+
ctor: { name: string; length: number };
|
|
6
|
+
deps: string[];
|
|
7
|
+
filePath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What the container hands a constructor is `deps`, one argument per key and in order — so a
|
|
12
|
+
* constructor asking for more is handed `undefined`. The scan reads `deps` off the signature and
|
|
13
|
+
* cannot miss; a frond declared by hand can, and `Function.length` is the one thing a
|
|
14
|
+
* constructor says about itself once the types are gone.
|
|
15
|
+
*
|
|
16
|
+
* `length` is a floor and never a count: a class that writes no constructor inherits
|
|
17
|
+
* `(...args) => super(...args)`, which is 0, while the scan read its parent's dependencies.
|
|
18
|
+
*/
|
|
19
|
+
export function refuseArityDrift(frond: FrondDescriptor, refused: Diagnostic[]): void {
|
|
20
|
+
const constructed: Constructed[] = [
|
|
21
|
+
...frond.handlers,
|
|
22
|
+
...frond.presenters,
|
|
23
|
+
...frond.collectors,
|
|
24
|
+
...frond.providers,
|
|
25
|
+
...frond.middlewares,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const { ctor, deps, filePath } of constructed) {
|
|
29
|
+
if (ctor.length <= deps.length) continue;
|
|
30
|
+
|
|
31
|
+
refused.push({
|
|
32
|
+
severity: 'blocking',
|
|
33
|
+
code: 'constructor-arity',
|
|
34
|
+
filePath,
|
|
35
|
+
frond: frond.name,
|
|
36
|
+
subject: ctor.name,
|
|
37
|
+
message: `${ctor.name}'s constructor takes ${ctor.length} parameter(s) and ${deps.length} `
|
|
38
|
+
+ `dependenc${deps.length === 1 ? 'y is' : 'ies are'} declared (${deps.join(', ') || 'none'}) — `
|
|
39
|
+
+ 'the rest would be handed `undefined`.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/boot/bootstrap.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { hostedBy } from './hosted.js';
|
|
|
7
7
|
import { nestingOf, parentsFirst } from './nesting.js';
|
|
8
8
|
import type { Fronds } from '../descriptor/Fronds.js';
|
|
9
9
|
import { installFrond, type Assembly } from './install.js';
|
|
10
|
-
import { dependentsOf, releasing, unfinishable, unheldAmong } from './relations.js';
|
|
10
|
+
import { dependentsOf, releasing, unfinishable, unheldAmong, unpaired } from './relations.js';
|
|
11
11
|
import { release as releaseRow } from '../dispatch/Release.js';
|
|
12
12
|
import type { Hosting } from './Hosting.js';
|
|
13
13
|
import { peerOver } from './peerOver.js';
|
|
@@ -448,6 +448,7 @@ function readings(
|
|
|
448
448
|
+ `Call its operations through the façade instead.`,
|
|
449
449
|
);
|
|
450
450
|
}
|
|
451
|
+
|
|
451
452
|
return route.schema;
|
|
452
453
|
}
|
|
453
454
|
throw new Error(notLoaded(entity));
|
|
@@ -489,6 +490,7 @@ function readings(
|
|
|
489
490
|
const owner = fronds.owner(entity);
|
|
490
491
|
if (!owner) {
|
|
491
492
|
sayNoSurfaceAcross(entity, surface);
|
|
493
|
+
|
|
492
494
|
return own;
|
|
493
495
|
}
|
|
494
496
|
|
|
@@ -498,6 +500,7 @@ function readings(
|
|
|
498
500
|
if (own) return own;
|
|
499
501
|
|
|
500
502
|
const fallback = facadeAt(facadeKeyOf(entity), false);
|
|
503
|
+
|
|
501
504
|
return fallback
|
|
502
505
|
? facadeOperations(
|
|
503
506
|
localDispatcher,
|
|
@@ -515,6 +518,7 @@ function readings(
|
|
|
515
518
|
const own = effectiveByKey.get(facadeKeyOf(entity, surface));
|
|
516
519
|
const declared = fronds.owner(entity)?.surfaces?.[surface];
|
|
517
520
|
if (!declared) return own;
|
|
521
|
+
|
|
518
522
|
return declared.some((name) => name.toLowerCase() === entity.toLowerCase())
|
|
519
523
|
? (own ?? effectiveByKey.get(facadeKeyOf(entity)))
|
|
520
524
|
: undefined;
|
|
@@ -699,6 +703,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
699
703
|
for (const frond of fronds) await installFrond(frond, assembly);
|
|
700
704
|
|
|
701
705
|
warnAboutRelations(relations, hosting, log);
|
|
706
|
+
refused.push(...unpaired(fronds.flatMap((frond) => frond.entities), hosting));
|
|
702
707
|
|
|
703
708
|
// Every check of the install, said at once — a boot that stops at the first makes the
|
|
704
709
|
// next one visible only after a fix and a restart.
|
|
@@ -735,6 +740,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
735
740
|
container.setFallback?.((name) => {
|
|
736
741
|
if (!remoteRouter) return undefined;
|
|
737
742
|
if (!name.endsWith('Handler') || name.includes(':')) return undefined;
|
|
743
|
+
|
|
738
744
|
// Façade-shaped stand-in; routing happens lazily at the first call. Through
|
|
739
745
|
// `lowerFirst` because a DEPENDENCY names the type as written — `ProductHandler`,
|
|
740
746
|
// PascalCase — while a card declares `product`, so the raw strip asked the router for
|
|
@@ -752,6 +758,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
752
758
|
if (known) return known;
|
|
753
759
|
const facade = createRemoteFacade(entity, remoteRouter, getMiddlewares);
|
|
754
760
|
remoteFacades.set(entity, facade);
|
|
761
|
+
|
|
755
762
|
return facade;
|
|
756
763
|
}));
|
|
757
764
|
}
|
|
@@ -769,7 +776,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
769
776
|
// Where a call goes, as DECLARED. Kept because a reader needs it beside what the
|
|
770
777
|
// runtime OBSERVED — `rpc.topology` calls a frond remote because it answered, never
|
|
771
778
|
// because a key said so, and the two disagree exactly when something is misconfigured.
|
|
772
|
-
remotes: Object.freeze({ ...
|
|
779
|
+
remotes: Object.freeze({ ...options.remotes }),
|
|
773
780
|
dispatch: (call) => dispatcher.dispatch(call),
|
|
774
781
|
local: localDispatcher,
|
|
775
782
|
resolve,
|
package/src/boot/card.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function identityCardOf(app: App, surface?: string): IdentityCard {
|
|
|
37
37
|
const ops = facadeOps(app, address, surface);
|
|
38
38
|
if (ops.length === 0) return [];
|
|
39
39
|
const entity = byEntity.get(address);
|
|
40
|
+
|
|
40
41
|
return [{
|
|
41
42
|
name: address,
|
|
42
43
|
ops,
|
|
@@ -47,6 +48,7 @@ export function identityCardOf(app: App, surface?: string): IdentityCard {
|
|
|
47
48
|
/** What leaves on its own — the same list on every surface, deliberately. */
|
|
48
49
|
facts: factsAnnouncedBy(frond.handlers).map((name) => {
|
|
49
50
|
const entityClass = declared.get(name);
|
|
51
|
+
|
|
50
52
|
return { name, ...(entityClass ? { schema: Card.fromSchema(entityClass, name).descriptor } : {}) };
|
|
51
53
|
}),
|
|
52
54
|
};
|
package/src/boot/frame.ts
CHANGED
|
@@ -83,6 +83,7 @@ function recordCreate({ base, recorded, entity, key, journal }: Recording): void
|
|
|
83
83
|
const row = await base.create.apply(this, args);
|
|
84
84
|
const id = String(row[key]);
|
|
85
85
|
journal.push({ what: `create ${entity}#${id}`, run: () => base.delete.call(this, id).then(() => undefined) });
|
|
86
|
+
|
|
86
87
|
return row;
|
|
87
88
|
};
|
|
88
89
|
}
|
|
@@ -119,6 +120,7 @@ function recordUpdate({ base, recorded, entity, journal }: Recording): void {
|
|
|
119
120
|
await base.update.call(this, id, pick(before, touched));
|
|
120
121
|
},
|
|
121
122
|
});
|
|
123
|
+
|
|
122
124
|
return row;
|
|
123
125
|
};
|
|
124
126
|
}
|
|
@@ -131,6 +133,7 @@ function recordDelete({ base, recorded, entity, journal }: Recording): void {
|
|
|
131
133
|
if (removed && before) {
|
|
132
134
|
journal.push({ what: `delete ${entity}#${id}`, run: () => base.create.call(this, before).then(() => undefined) });
|
|
133
135
|
}
|
|
136
|
+
|
|
134
137
|
return removed;
|
|
135
138
|
};
|
|
136
139
|
}
|
|
@@ -144,6 +147,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
|
|
|
144
147
|
const undoUpsert = async function (this: unknown, rows: readonly Record<string, unknown>[]): Promise<Undo> {
|
|
145
148
|
const ids = rows.map((row) => String(row[key]));
|
|
146
149
|
const before = await base.findByKeys.call(this, ids);
|
|
150
|
+
|
|
147
151
|
return {
|
|
148
152
|
what: `upsert ${entity} (${ids.length} row(s))`,
|
|
149
153
|
run: async () => {
|
|
@@ -163,6 +167,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
|
|
|
163
167
|
const undo = await undoUpsert.call(this, [input]);
|
|
164
168
|
const row = await upsert.call(this, input, ...rest);
|
|
165
169
|
journal.push(undo);
|
|
170
|
+
|
|
166
171
|
return row;
|
|
167
172
|
};
|
|
168
173
|
}
|
|
@@ -174,6 +179,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
|
|
|
174
179
|
const undo = await undoUpsert.call(this, inputs);
|
|
175
180
|
const written = await upsertAll.call(this, inputs, ...rest);
|
|
176
181
|
journal.push(undo);
|
|
182
|
+
|
|
177
183
|
return written;
|
|
178
184
|
};
|
|
179
185
|
}
|
package/src/boot/install.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { Diagnostic } from '../diagnostic.js';
|
|
|
21
21
|
import { HandlerFacade } from '../dispatch/HandlerFacade.js';
|
|
22
22
|
import { targetOf } from '../prefab/prefab.js';
|
|
23
23
|
import { ownersOf, sharedNames, storageInUserCode, crudOnOwned } from './ownership.js';
|
|
24
|
+
import { refuseArityDrift } from './arity.js';
|
|
24
25
|
import { StorageGuard } from '../dispatch/StorageGuard.js';
|
|
25
26
|
import { portBindings, seamChains, wrapping, SEAMS } from './ports.js';
|
|
26
27
|
import { contractsKeyOf, facadeKeyOf } from '../wire/Facade.js';
|
|
@@ -37,6 +38,7 @@ import { RouteAddress } from '../wire/RouteAddress.js';
|
|
|
37
38
|
import { servedSurfaces } from '../descriptor/surface.js';
|
|
38
39
|
import { OperationRoute } from '../dispatch/OperationRoute.js';
|
|
39
40
|
import { facadeOperations } from '../entry/facade.js';
|
|
41
|
+
import { decoded } from '../dispatch/decoded.js';
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* What the app is made of while it is still being made. `createApp` builds these, hands
|
|
@@ -306,7 +308,7 @@ function registerProviders(
|
|
|
306
308
|
// of: the container resolves a dep by NAME, so wrapping is a substituted key and
|
|
307
309
|
// needs nothing of the container itself. The outermost answers under the port.
|
|
308
310
|
let inner = nameOf(chain.at(-1)!);
|
|
309
|
-
for (const wrapper of
|
|
311
|
+
for (const wrapper of chain.slice(0, -1).reverse()) {
|
|
310
312
|
const deps = wrapper.deps.map((dep) => (dep === port ? inner : dep));
|
|
311
313
|
inner = nameOf(wrapper);
|
|
312
314
|
scope.register(inner, wrapper.ctor, { deps });
|
|
@@ -449,6 +451,9 @@ function buildFacadeInto(
|
|
|
449
451
|
handler.address,
|
|
450
452
|
routeRegistry.operationNames(handler.address, handler.surface),
|
|
451
453
|
handler.surface,
|
|
454
|
+
// The output SCHEMA, per op: `Crud(Post, { list: PostCard })` names a different one for
|
|
455
|
+
// one operation, so the answer is read back through the view that op actually serves.
|
|
456
|
+
(operation, answer) => decoded(facade.effectiveOperations.get(operation)?.output, answer),
|
|
452
457
|
));
|
|
453
458
|
}
|
|
454
459
|
|
|
@@ -471,6 +476,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
471
476
|
emissions.note(contractsOf(operations), key);
|
|
472
477
|
|
|
473
478
|
}
|
|
479
|
+
|
|
474
480
|
return;
|
|
475
481
|
}
|
|
476
482
|
// A child hangs off its parent, so everything the parent registered answers here too and
|
|
@@ -495,6 +501,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
495
501
|
// Who owns what, and the rule that makes owning mean something. Before anything is
|
|
496
502
|
// registered, so a bad line is named by this refusal rather than by the container's.
|
|
497
503
|
sharedNames(frond, refused);
|
|
504
|
+
refuseArityDrift(frond, refused);
|
|
498
505
|
const owners = ownersOf(frond.providers, frond.name, refused);
|
|
499
506
|
storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
|
|
500
507
|
crudOnOwned(frond, owners, refused);
|
package/src/boot/ownership.ts
CHANGED
|
@@ -80,6 +80,7 @@ function builtOn(ctor: unknown): string[] {
|
|
|
80
80
|
const owned = ownedBy(ctor);
|
|
81
81
|
if (owned.length > 0) return owned.map((e) => lowerFirst((e as { name: string }).name));
|
|
82
82
|
const target = targetOf(ctor) as { name?: string } | undefined;
|
|
83
|
+
|
|
83
84
|
return target?.name ? [lowerFirst(target.name)] : [];
|
|
84
85
|
}
|
|
85
86
|
|
package/src/boot/relations.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { RelationCheck } from '../dispatch/RelationCheck.js';
|
|
|
5
5
|
import type { Hosting } from './Hosting.js';
|
|
6
6
|
import type { Diagnostic } from '../diagnostic.js';
|
|
7
7
|
import type { Releasing, Rows } from '../dispatch/Release.js';
|
|
8
|
+
import type { EntityEntry } from '../descriptor/EntityEntry.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Does a foreign key hold this reference? Both sides in ONE source that keeps relations — two
|
|
@@ -34,7 +35,7 @@ function referencesTo(target: string, hosting: Hosting): { entity: string; field
|
|
|
34
35
|
for (const [entity, schema] of hosting.entities()) {
|
|
35
36
|
for (const [field, declared] of Object.entries(schema.getFields())) {
|
|
36
37
|
const role = Role.of(declared);
|
|
37
|
-
if (role.isReference && role.target?.name && lowerFirst(role.target.name) === target) {
|
|
38
|
+
if (role.isReference() && role.target?.name && lowerFirst(role.target.name) === target) {
|
|
38
39
|
found.push({ entity, field, role });
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -58,7 +59,7 @@ export function heldBy(entity: SchemaView, name: string, hosting: Hosting): Rela
|
|
|
58
59
|
|
|
59
60
|
for (const [field, declared] of Object.entries(entity.getFields())) {
|
|
60
61
|
const role = Role.of(declared);
|
|
61
|
-
if (!role.isReference || !role.target?.name) continue;
|
|
62
|
+
if (!role.isReference() || !role.target?.name) continue;
|
|
62
63
|
|
|
63
64
|
const target = lowerFirst(role.target.name);
|
|
64
65
|
if (keyed(name, target, hosting)) continue;
|
|
@@ -122,6 +123,42 @@ export function refuseUnwritableNull(entity: SchemaView, name: string, filePath:
|
|
|
122
123
|
return refused;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* A `many()` whose target declares no `ref()` back. The key lives on the far row, so a
|
|
128
|
+
* relation neither side states can never be filled: SQL emits no column, GraphQL drops the
|
|
129
|
+
* field when it finds no reverse key, and nothing says why.
|
|
130
|
+
*
|
|
131
|
+
* Asked once every frond is installed, since the target may belong to another one — and left
|
|
132
|
+
* alone when it answers elsewhere, where its fields are declared and cannot be read here.
|
|
133
|
+
*/
|
|
134
|
+
export function unpaired(entities: readonly EntityEntry[], hosting: Hosting): Diagnostic[] {
|
|
135
|
+
const refused: Diagnostic[] = [];
|
|
136
|
+
|
|
137
|
+
for (const entry of entities) {
|
|
138
|
+
for (const [field, declared] of Object.entries(entry.entityClass.getFields())) {
|
|
139
|
+
const role = Role.of(declared);
|
|
140
|
+
const target = role.isCollection() ? role.target?.name : undefined;
|
|
141
|
+
if (!target || !hosting.hostedHere(lowerFirst(target))) continue;
|
|
142
|
+
|
|
143
|
+
const held = referencesTo(lowerFirst(entry.name), hosting)
|
|
144
|
+
.some((one) => one.entity === lowerFirst(target));
|
|
145
|
+
if (held) continue;
|
|
146
|
+
|
|
147
|
+
refused.push({
|
|
148
|
+
severity: 'blocking',
|
|
149
|
+
code: 'many-without-reference',
|
|
150
|
+
filePath: entry.filePath,
|
|
151
|
+
subject: `${entry.name}.${field}`,
|
|
152
|
+
message: `${entry.entityClass.name}.${field} states many(${target}), and no field of `
|
|
153
|
+
+ `${target} references ${entry.entityClass.name}: the relation is declared on neither `
|
|
154
|
+
+ `side, so nothing can fill it. Declare the ref() on ${target}.`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return refused;
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
/** The checks nothing in this process can answer — asked once every frond has registered. */
|
|
126
163
|
export function unheldAmong(checks: readonly RelationCheck[], hosting: Hosting): string[] {
|
|
127
164
|
return checks
|
package/src/boot/remote.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { ErrorCode } from '../wire/ErrorCode.js';
|
|
|
15
15
|
import { FougereError } from '../wire/FougereError.js';
|
|
16
16
|
import { Card, type SchemaView, type SchemaDescriptor } from '@fougere/schema';
|
|
17
17
|
import { dynamicOperations } from '../entry/facade.js';
|
|
18
|
+
import { decoded } from '../dispatch/decoded.js';
|
|
18
19
|
|
|
19
20
|
interface Route {
|
|
20
21
|
frond: string;
|
|
@@ -53,6 +54,7 @@ export function createRemoteRouter(
|
|
|
53
54
|
transports.set(url, transport);
|
|
54
55
|
try {
|
|
55
56
|
const answer = await transport({ entity: RPC_ENTITY, op: 'discover' }, Invocation.empty);
|
|
57
|
+
|
|
56
58
|
// Judged below and not here: this catch means "unreachable, retry", and a
|
|
57
59
|
// refusal thrown inside it would be swallowed into another silent retry.
|
|
58
60
|
return { label, url, transport, answer };
|
|
@@ -103,13 +105,19 @@ export function createRemoteFacade(
|
|
|
103
105
|
middlewaresFor: (address: string) => AppMiddleware[],
|
|
104
106
|
): Facade {
|
|
105
107
|
const opFn = (op: string) => async (invocation: InvocationContext = Invocation.empty) => {
|
|
106
|
-
const { frond, transport } = await router.route(entity);
|
|
108
|
+
const { frond, transport, schema } = await router.route(entity);
|
|
107
109
|
const call: FrondCall = { frond, entity, op };
|
|
108
110
|
const ctx: OperationContext = {
|
|
109
111
|
entity, frond, operation: op, args: [], state: invocation.state, invocation,
|
|
110
112
|
};
|
|
111
|
-
|
|
113
|
+
const answer = await runMiddlewares(middlewaresFor(entity), ctx, () =>
|
|
112
114
|
transport(call, ctx.invocation ?? invocation));
|
|
115
|
+
|
|
116
|
+
// The schema the card carried, put to work: a row crosses as data and comes back through
|
|
117
|
+
// the same codecs a local facade applies, so a placement does not decide what a caller
|
|
118
|
+
// holds. It is the ENTITY's — an op serving a narrower view is the far side's business,
|
|
119
|
+
// and what this card names is the shape it publishes.
|
|
120
|
+
return decoded(schema, answer);
|
|
113
121
|
};
|
|
114
122
|
|
|
115
123
|
return dynamicOperations(opFn) as Facade;
|
package/src/boot/seed.ts
CHANGED
|
@@ -47,7 +47,7 @@ function whatEachWaitsFor(
|
|
|
47
47
|
for (const entity of frond.entities) {
|
|
48
48
|
const targets = new Set<string>();
|
|
49
49
|
for (const field of Object.values(entity.entityClass.getFields())) {
|
|
50
|
-
if (!Role.of(field).isReference) continue;
|
|
50
|
+
if (!Role.of(field).isReference()) continue;
|
|
51
51
|
const target = (Role.of(field).target as { name?: string }).name?.toLowerCase();
|
|
52
52
|
if (target && target !== entity.name.toLowerCase()) targets.add(target);
|
|
53
53
|
}
|
|
@@ -41,3 +41,28 @@ export function statementDrift(frond: FrondDescriptor, handler: HandlerEntry): D
|
|
|
41
41
|
|
|
42
42
|
return found;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A contract with no method under its name. The scan cannot produce one; a declaration can, and
|
|
47
|
+
* its first call used to fail as `instance[method] is not a function` — a server error for what
|
|
48
|
+
* is a typo in the declaration.
|
|
49
|
+
*/
|
|
50
|
+
export function unansweredOperations(
|
|
51
|
+
frond: FrondDescriptor,
|
|
52
|
+
handler: HandlerEntry,
|
|
53
|
+
names: Iterable<string>,
|
|
54
|
+
): Diagnostic[] {
|
|
55
|
+
const prototype = handler.ctor.prototype as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
return [...names]
|
|
58
|
+
.filter((name) => typeof prototype[name] !== 'function')
|
|
59
|
+
.map((name) => ({
|
|
60
|
+
severity: 'blocking',
|
|
61
|
+
code: 'operation-without-method',
|
|
62
|
+
filePath: handler.filePath,
|
|
63
|
+
frond: frond.name,
|
|
64
|
+
subject: `${handler.ctor.name}.${name}`,
|
|
65
|
+
message: `${handler.ctor.name}.${name} is declared as an operation, and ${handler.ctor.name} `
|
|
66
|
+
+ 'has no method of that name — its first call would fail as a server error.',
|
|
67
|
+
}));
|
|
68
|
+
}
|
package/src/boot/together.ts
CHANGED
|
@@ -154,13 +154,16 @@ async function inScope<R>(
|
|
|
154
154
|
const storages = members.entities.map((member) => {
|
|
155
155
|
const storage = wrap(factory(member.schema, member.name), member.name, member.schema);
|
|
156
156
|
scope.registerValue(storageKeyOf(member.name), storage);
|
|
157
|
+
|
|
157
158
|
return storage;
|
|
158
159
|
});
|
|
159
160
|
// Providers after every storage is in place: one may depend on another member's.
|
|
160
161
|
const built = members.providers.map((provider) => {
|
|
161
162
|
scope.register(provider.ctor.name, provider.ctor, { deps: provider.deps });
|
|
163
|
+
|
|
162
164
|
return scope.resolve(provider.ctor.name);
|
|
163
165
|
});
|
|
166
|
+
|
|
164
167
|
return await fn(storages, built);
|
|
165
168
|
} finally {
|
|
166
169
|
await scope.dispose();
|