@fougere/core 0.12.1-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 +2 -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 +3 -1
- 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.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.map +1 -1
- 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/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/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.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.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 +1 -1
- 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.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +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.map +1 -1
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.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 +19 -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 +4 -1
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +39 -2
- package/src/boot/remote.ts +1 -0
- 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 +1 -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/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/identity-keys.ts +2 -0
- package/src/loader.ts +1 -0
- package/src/prefab/CrudConstructor.ts +1 -0
- 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 +9 -1
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +2 -0
- package/src/wire/FougereError.ts +2 -0
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +1 -0
- package/src/wire/RouteAddress.ts +2 -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
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';
|
|
@@ -307,7 +308,7 @@ function registerProviders(
|
|
|
307
308
|
// of: the container resolves a dep by NAME, so wrapping is a substituted key and
|
|
308
309
|
// needs nothing of the container itself. The outermost answers under the port.
|
|
309
310
|
let inner = nameOf(chain.at(-1)!);
|
|
310
|
-
for (const wrapper of
|
|
311
|
+
for (const wrapper of chain.slice(0, -1).reverse()) {
|
|
311
312
|
const deps = wrapper.deps.map((dep) => (dep === port ? inner : dep));
|
|
312
313
|
inner = nameOf(wrapper);
|
|
313
314
|
scope.register(inner, wrapper.ctor, { deps });
|
|
@@ -475,6 +476,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
475
476
|
emissions.note(contractsOf(operations), key);
|
|
476
477
|
|
|
477
478
|
}
|
|
479
|
+
|
|
478
480
|
return;
|
|
479
481
|
}
|
|
480
482
|
// A child hangs off its parent, so everything the parent registered answers here too and
|
|
@@ -499,6 +501,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
499
501
|
// Who owns what, and the rule that makes owning mean something. Before anything is
|
|
500
502
|
// registered, so a bad line is named by this refusal rather than by the container's.
|
|
501
503
|
sharedNames(frond, refused);
|
|
504
|
+
refuseArityDrift(frond, refused);
|
|
502
505
|
const owners = ownersOf(frond.providers, frond.name, refused);
|
|
503
506
|
storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
|
|
504
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
|
@@ -54,6 +54,7 @@ export function createRemoteRouter(
|
|
|
54
54
|
transports.set(url, transport);
|
|
55
55
|
try {
|
|
56
56
|
const answer = await transport({ entity: RPC_ENTITY, op: 'discover' }, Invocation.empty);
|
|
57
|
+
|
|
57
58
|
// Judged below and not here: this catch means "unreachable, retry", and a
|
|
58
59
|
// refusal thrown inside it would be swallowed into another silent retry.
|
|
59
60
|
return { label, url, transport, answer };
|
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();
|
package/src/builtin/Carry.ts
CHANGED
package/src/builtin/Logger.ts
CHANGED
|
@@ -12,6 +12,7 @@ let threshold: number = LEVELS[envLevel() ?? 'info'];
|
|
|
12
12
|
/** The level the PROCESS was started with. */
|
|
13
13
|
export function envLevel(): LogLevel | undefined {
|
|
14
14
|
const raw = typeof process === 'undefined' ? undefined : process.env.FOUGERE_LOG_LEVEL;
|
|
15
|
+
|
|
15
16
|
return raw !== undefined && raw in LEVELS ? (raw as LogLevel) : undefined;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -51,8 +52,10 @@ function supportsColor(): boolean {
|
|
|
51
52
|
if (typeof process !== 'undefined') {
|
|
52
53
|
if (process.env.NO_COLOR) return false;
|
|
53
54
|
if (process.env.FORCE_COLOR === '1') return true;
|
|
55
|
+
|
|
54
56
|
return process.stdout?.isTTY === true;
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
return false;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -62,6 +65,7 @@ function stamp(at: number | Date): string {
|
|
|
62
65
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
63
66
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
64
67
|
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
68
|
+
|
|
65
69
|
return `${h}:${m}:${s}.${ms}`;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -92,7 +96,7 @@ export class Logger {
|
|
|
92
96
|
level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now(),
|
|
93
97
|
};
|
|
94
98
|
|
|
95
|
-
this.carry?.push(record);
|
|
99
|
+
this.carry?.push({ ...record, args: args.map(objectOf) });
|
|
96
100
|
|
|
97
101
|
// The console ALWAYS, whoever else took the line. Skipping it once a destination
|
|
98
102
|
// existed made `calls()` — a devtools ring that prints nothing — silence the
|
|
@@ -103,6 +107,10 @@ export class Logger {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/** A line travels as a fact, where `args` states `json()` — an object. */
|
|
111
|
+
const objectOf = (arg: unknown): object =>
|
|
112
|
+
typeof arg === 'object' && arg !== null && !Array.isArray(arg) ? arg : { value: arg };
|
|
113
|
+
|
|
106
114
|
export function formatted(
|
|
107
115
|
record: Rendered,
|
|
108
116
|
color = supportsColor(),
|
package/src/builtin/config.ts
CHANGED
package/src/contract.ts
CHANGED
package/src/crypto/encoding.ts
CHANGED
|
@@ -13,6 +13,7 @@ export function b64url(input: Uint8Array | string): string {
|
|
|
13
13
|
// One character at a time: `String.fromCharCode(...bytes)` overflows the argument
|
|
14
14
|
// limit on a key, and a signature is small enough that the loop costs nothing.
|
|
15
15
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
16
|
+
|
|
16
17
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export function unb64url(input: string): Uint8Array {
|
|
|
20
21
|
const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/'));
|
|
21
22
|
const bytes = new Uint8Array(binary.length);
|
|
22
23
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
|
|
23
25
|
return bytes;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -28,6 +30,7 @@ export const unb64 = (input: string): Uint8Array => {
|
|
|
28
30
|
const binary = atob(input);
|
|
29
31
|
const bytes = new Uint8Array(binary.length);
|
|
30
32
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
|
|
31
34
|
return bytes;
|
|
32
35
|
};
|
|
33
36
|
|
|
@@ -35,5 +38,6 @@ export const unb64 = (input: string): Uint8Array => {
|
|
|
35
38
|
export function derOf(pem: string): Uint8Array {
|
|
36
39
|
const body = pem.replace(/-----(BEGIN|END)[^-]+-----/g, '').replace(/\s+/g, '');
|
|
37
40
|
if (body.length === 0) throw new Error('Not a PEM: no base64 body between the armour');
|
|
41
|
+
|
|
38
42
|
return unb64(body);
|
|
39
43
|
}
|
package/src/crypto/node.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const crypto: CryptoPort = {
|
|
|
13
13
|
async signerOf(privateKeyPem): Promise<Signer> {
|
|
14
14
|
// Parsed here and captured, so a call signs against a KeyObject and never a string.
|
|
15
15
|
const key = createPrivateKey(privateKeyPem);
|
|
16
|
+
|
|
16
17
|
return { async sign(data) { return new Uint8Array(sign(null, data, key)); } };
|
|
17
18
|
},
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export const crypto: CryptoPort = {
|
|
|
20
21
|
const parsed = typeof key === 'string'
|
|
21
22
|
? createPublicKey(key)
|
|
22
23
|
: createPublicKey({ key: key as never, format: 'jwk' });
|
|
24
|
+
|
|
23
25
|
return { async verify(data, signature) { return verify(null, data, parsed, signature); } };
|
|
24
26
|
},
|
|
25
27
|
};
|
package/src/crypto/webcrypto.ts
CHANGED
|
@@ -33,6 +33,7 @@ export const crypto: CryptoPort = {
|
|
|
33
33
|
// Imported once, at boot. Doing it per call would put an async key parse inside
|
|
34
34
|
// every outgoing request — the reason the port hands back a signer at all.
|
|
35
35
|
const key = await subtle().importKey('pkcs8', derOf(privateKeyPem), ED25519, false, ['sign']);
|
|
36
|
+
|
|
36
37
|
return { async sign(data) { return new Uint8Array(await subtle().sign(ED25519, key, data)); } };
|
|
37
38
|
},
|
|
38
39
|
|
|
@@ -40,6 +41,7 @@ export const crypto: CryptoPort = {
|
|
|
40
41
|
const imported = typeof key === 'string'
|
|
41
42
|
? await subtle().importKey('spki', derOf(key), ED25519, false, ['verify'])
|
|
42
43
|
: await subtle().importKey('jwk', key, ED25519, false, ['verify']);
|
|
44
|
+
|
|
43
45
|
return { verify: (data, signature) => subtle().verify(ED25519, imported, signature, data) };
|
|
44
46
|
},
|
|
45
47
|
};
|
package/src/descriptor/Fronds.ts
CHANGED
|
@@ -12,6 +12,7 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
12
12
|
static hosting(fronds: readonly FrondDescriptor[]): Fronds {
|
|
13
13
|
const all = new Fronds();
|
|
14
14
|
all.push(...fronds);
|
|
15
|
+
|
|
15
16
|
return all;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -26,6 +27,7 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
26
27
|
const found = frond.entities.find((e) => e.name === name);
|
|
27
28
|
if (found) return found;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
return undefined;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -71,6 +71,7 @@ export class Dispatcher implements DispatchPort {
|
|
|
71
71
|
this.lifecycle.publish(DispatchEvent.resolved(call, route.kind));
|
|
72
72
|
const result = await route.execute(call);
|
|
73
73
|
this.lifecycle.publish(DispatchEvent.completed(call, route.kind));
|
|
74
|
+
|
|
74
75
|
return result;
|
|
75
76
|
} catch (error) {
|
|
76
77
|
this.lifecycle.publish(DispatchEvent.failed(call, error, route?.kind));
|
|
@@ -116,15 +116,23 @@ export class HandlerFacade {
|
|
|
116
116
|
|
|
117
117
|
const args = contract.binding ? await this.arguments.resolve(contract.binding, validated) : [];
|
|
118
118
|
const { instance, method } = this.resolveImplementation(op);
|
|
119
|
-
const
|
|
120
|
-
const output = view.project(await instance[method](...args));
|
|
119
|
+
const answered = await instance[method](...args);
|
|
121
120
|
|
|
121
|
+
const view = this.viewOf(op);
|
|
122
122
|
const { presenter } = this.facade;
|
|
123
|
+
const computed = view.closed || !presenter
|
|
124
|
+
? answered
|
|
125
|
+
: await this.present(op, presenter, answered, validated);
|
|
123
126
|
|
|
124
|
-
return view.
|
|
127
|
+
return view.project(computed);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The computed fields a presenter adds, over the rows the handler answered and BEFORE the
|
|
132
|
+
* view projects them. A presenter runs on the server, so what it reads is the row the
|
|
133
|
+
* storage holds: a `writeOnly` field is gone by the time the client sees it, and deriving
|
|
134
|
+
* from one is the whole reason to declare it.
|
|
135
|
+
*/
|
|
128
136
|
private present(
|
|
129
137
|
op: string,
|
|
130
138
|
presenter: PresenterEntry,
|
|
@@ -180,12 +188,14 @@ export class HandlerFacade {
|
|
|
180
188
|
operation?.outputClosed ?? false,
|
|
181
189
|
);
|
|
182
190
|
this.cachedViews.set(op, resolved);
|
|
191
|
+
|
|
183
192
|
return resolved;
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
/** The handler itself, resolved on first call — never at boot. */
|
|
187
196
|
private resolveHandler(): any {
|
|
188
197
|
if (!this.instance) this.instance = this.scope.resolve(this.handlerKey);
|
|
198
|
+
|
|
189
199
|
return this.instance;
|
|
190
200
|
}
|
|
191
201
|
|
|
@@ -208,6 +218,7 @@ export class HandlerFacade {
|
|
|
208
218
|
+ `${operation.implementation.method}, but boot found ${matches.length} matching handlers.`,
|
|
209
219
|
);
|
|
210
220
|
}
|
|
221
|
+
|
|
211
222
|
return matches[0]!;
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -225,6 +236,7 @@ export class HandlerFacade {
|
|
|
225
236
|
instance = this.scope.resolve(key);
|
|
226
237
|
this.implementationInstances.set(key, instance);
|
|
227
238
|
}
|
|
239
|
+
|
|
228
240
|
return { instance, method: operation.implementation.method };
|
|
229
241
|
}
|
|
230
242
|
}
|
package/src/dispatch/InFlight.ts
CHANGED
|
@@ -23,6 +23,7 @@ export class InFlight {
|
|
|
23
23
|
|
|
24
24
|
this.running++;
|
|
25
25
|
let released = false;
|
|
26
|
+
|
|
26
27
|
return () => {
|
|
27
28
|
if (released) return;
|
|
28
29
|
released = true;
|
|
@@ -41,6 +42,7 @@ export class InFlight {
|
|
|
41
42
|
|
|
42
43
|
whenIdle(): Promise<void> {
|
|
43
44
|
if (this.running === 0) return Promise.resolve();
|
|
45
|
+
|
|
44
46
|
return new Promise((resolve) => this.idle.push(resolve));
|
|
45
47
|
}
|
|
46
48
|
}
|