@fougere/core 0.10.0-alpha.0 → 0.11.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.
- package/dist/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js +2 -0
- package/dist/CallIdentity.js.map +1 -1
- package/dist/Constraint.d.ts +1 -1
- package/dist/Constraint.d.ts.map +1 -1
- package/dist/Source.d.ts.map +1 -1
- package/dist/Source.js +4 -3
- package/dist/Source.js.map +1 -1
- package/dist/boot/Hosting.d.ts +38 -0
- package/dist/boot/Hosting.d.ts.map +1 -0
- package/dist/boot/Hosting.js +2 -0
- package/dist/boot/Hosting.js.map +1 -0
- package/dist/boot/Peer.d.ts +16 -0
- package/dist/boot/Peer.d.ts.map +1 -0
- package/dist/boot/Peer.js +2 -0
- package/dist/boot/Peer.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +122 -20
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/declared.d.ts.map +1 -1
- package/dist/boot/declared.js +35 -3
- package/dist/boot/declared.js.map +1 -1
- package/dist/boot/install.d.ts +10 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +11 -2
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/peerOver.d.ts +4 -0
- package/dist/boot/peerOver.d.ts.map +1 -0
- package/dist/boot/peerOver.js +12 -0
- package/dist/boot/peerOver.js.map +1 -0
- package/dist/boot/relations.d.ts +54 -0
- package/dist/boot/relations.d.ts.map +1 -0
- package/dist/boot/relations.js +150 -0
- package/dist/boot/relations.js.map +1 -0
- package/dist/boot/together.d.ts +3 -0
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +2 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/dispatch/Dependent.d.ts +19 -0
- package/dist/dispatch/Dependent.d.ts.map +1 -0
- package/dist/dispatch/Dependent.js +2 -0
- package/dist/dispatch/Dependent.js.map +1 -0
- package/dist/dispatch/Dispatcher.d.ts +14 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js +36 -8
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/Journal.d.ts +25 -0
- package/dist/dispatch/Journal.d.ts.map +1 -0
- package/dist/dispatch/Journal.js +3 -0
- package/dist/dispatch/Journal.js.map +1 -0
- package/dist/dispatch/RelationCheck.d.ts +22 -0
- package/dist/dispatch/RelationCheck.d.ts.map +1 -0
- package/dist/dispatch/RelationCheck.js +2 -0
- package/dist/dispatch/RelationCheck.js.map +1 -0
- package/dist/dispatch/Release.d.ts +36 -0
- package/dist/dispatch/Release.d.ts.map +1 -0
- package/dist/dispatch/Release.js +124 -0
- package/dist/dispatch/Release.js.map +1 -0
- package/dist/dispatch/StorageGuard.d.ts +18 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +57 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/wire/Emit.d.ts +9 -0
- package/dist/wire/Emit.d.ts.map +1 -1
- package/dist/wire/Emit.js +16 -0
- package/dist/wire/Emit.js.map +1 -1
- package/dist/wire/Invocation.d.ts +2 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +4 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/InvocationContext.d.ts +2 -0
- package/dist/wire/InvocationContext.d.ts.map +1 -1
- package/dist/wire/SignedCall.d.ts +2 -0
- package/dist/wire/SignedCall.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/CallIdentity.ts +2 -0
- package/src/Constraint.ts +1 -1
- package/src/Source.ts +3 -2
- package/src/boot/Hosting.ts +38 -0
- package/src/boot/Peer.ts +15 -0
- package/src/boot/bootstrap.ts +142 -20
- package/src/boot/declared.ts +35 -3
- package/src/boot/install.ts +23 -2
- package/src/boot/peerOver.ts +17 -0
- package/src/boot/relations.ts +172 -0
- package/src/boot/together.ts +5 -1
- package/src/contract.ts +1 -0
- package/src/dispatch/Dependent.ts +19 -0
- package/src/dispatch/Dispatcher.ts +43 -7
- package/src/dispatch/Journal.ts +26 -0
- package/src/dispatch/RelationCheck.ts +21 -0
- package/src/dispatch/Release.ts +156 -0
- package/src/dispatch/StorageGuard.ts +62 -0
- package/src/index.ts +5 -0
- package/src/wire/Emit.ts +23 -0
- package/src/wire/Invocation.ts +3 -1
- package/src/wire/InvocationContext.ts +2 -0
- package/src/wire/SignedCall.ts +2 -0
package/dist/wire/Emit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lowerFirst } from '@fougere/schema';
|
|
2
|
+
import { computeBindingPlan } from './binding.js';
|
|
2
3
|
/** The container key of an emission — THE one place that spells the format. */
|
|
3
4
|
export function emitKeyOf(fact) {
|
|
4
5
|
return `${lowerFirst(fact)}Emit`;
|
|
@@ -34,6 +35,21 @@ export function factsAnnouncedBy(handlers) {
|
|
|
34
35
|
export function factsAwaitedBy(handlers) {
|
|
35
36
|
return factsIn(handlers, factOfAwaitKey);
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* What a set of handlers LISTENS to — the dual of what it announces, and the half no
|
|
40
|
+
* dependency spells: accepting a `Fact<T>` IS the subscription, so it is read from the
|
|
41
|
+
* binding plan, where `Emissions` reads it too.
|
|
42
|
+
*/
|
|
43
|
+
export function factsListenedTo(handlers) {
|
|
44
|
+
const taken = (contract) => {
|
|
45
|
+
const plan = contract.binding
|
|
46
|
+
?? (contract.signature ? computeBindingPlan(contract.signature.params, NO_COLLECTOR) : []);
|
|
47
|
+
return plan.flatMap((bound) => bound.source.kind === 'fact' || bound.source.kind === 'pipe' ? [bound.source.factName] : []);
|
|
48
|
+
};
|
|
49
|
+
return [...new Set(handlers.flatMap((handler) => [...handler.operations.values()].flatMap(taken)))];
|
|
50
|
+
}
|
|
51
|
+
/** `Fact` and `Pipe` name themselves, and the plan reads them before it reads a collector. */
|
|
52
|
+
const NO_COLLECTOR = new Set();
|
|
37
53
|
function factsIn(handlers, behindKey) {
|
|
38
54
|
return [...new Set(handlers
|
|
39
55
|
.flatMap((handler) => handler.deps)
|
package/dist/wire/Emit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Emit.js","sourceRoot":"","sources":["../../src/wire/Emit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"Emit.js","sourceRoot":"","sources":["../../src/wire/Emit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAYlD,+EAA+E;AAC/E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;AACpC,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB,MAAM,YAAY,GAAG,OAAO,CAAC;AAE7B,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,MAAM,CAAC,GAAW,EAAE,MAAc;IACzC,OAAO,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QACvD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QAC9B,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,QAAgD;IAC/E,OAAO,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AAC1C,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,cAAc,CAAC,QAAgD;IAC7E,OAAO,OAAO,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AAC3C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,QAAkD;IAChF,MAAM,KAAK,GAAG,CAAC,QAA2B,EAAY,EAAE;QACtD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO;eACxB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAE7F,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5B,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACjG,CAAC,CAAC;IAEF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACtG,CAAC;AAED,8FAA8F;AAC9F,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;AAEvC,SAAS,OAAO,CACd,QAAgD,EAChD,SAA8C;IAE9C,OAAO,CAAC,GAAG,IAAI,GAAG,CAChB,QAAQ;aACL,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,GAAG,CAAC,SAAS,CAAC;aACd,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CACxD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -9,7 +9,8 @@ export declare class Invocation implements InvocationContext {
|
|
|
9
9
|
readonly trace?: string;
|
|
10
10
|
readonly identity?: string;
|
|
11
11
|
readonly caller?: string;
|
|
12
|
-
|
|
12
|
+
readonly runAt?: number;
|
|
13
|
+
/** Gives every entry and every transport the same eight-member value, frozen. */
|
|
13
14
|
private constructor();
|
|
14
15
|
/** Takes a raw object or an invocation, so a caller never has to know which it holds. */
|
|
15
16
|
static from(context?: PartialInvocation): Invocation;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Invocation.d.ts","sourceRoot":"","sources":["../../src/wire/Invocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAuBhE,gEAAgE;AAChE,qBAAa,UAAW,YAAW,iBAAiB;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"Invocation.d.ts","sourceRoot":"","sources":["../../src/wire/Invocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAuBhE,gEAAgE;AAChE,qBAAa,UAAW,YAAW,iBAAiB;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,iFAAiF;IACjF,OAAO;IAcP,yFAAyF;IACzF,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,UAAU;IAIpD,MAAM,CAAC,QAAQ,CAAC,KAAK,aAAqB;IAE1C,wEAAwE;IACxE,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU;IAIrC,0FAA0F;IAC1F,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU;CAGtD"}
|
package/dist/wire/Invocation.js
CHANGED
|
@@ -29,7 +29,8 @@ export class Invocation {
|
|
|
29
29
|
trace;
|
|
30
30
|
identity;
|
|
31
31
|
caller;
|
|
32
|
-
|
|
32
|
+
runAt;
|
|
33
|
+
/** Gives every entry and every transport the same eight-member value, frozen. */
|
|
33
34
|
constructor(context) {
|
|
34
35
|
this.params = canonicalRecord(context.params);
|
|
35
36
|
this.query = canonicalRecord(context.query);
|
|
@@ -43,6 +44,8 @@ export class Invocation {
|
|
|
43
44
|
this.identity = context.identity;
|
|
44
45
|
if (context.caller !== undefined)
|
|
45
46
|
this.caller = context.caller;
|
|
47
|
+
if (context.runAt !== undefined)
|
|
48
|
+
this.runAt = context.runAt;
|
|
46
49
|
Object.freeze(this);
|
|
47
50
|
}
|
|
48
51
|
/** Takes a raw object or an invocation, so a caller never has to know which it holds. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Invocation.js","sourceRoot":"","sources":["../../src/wire/Invocation.ts"],"names":[],"mappings":"AAGA,+FAA+F;AAC/F,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACrF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;IAE1E,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACvE,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,oFAAoF;AACpF,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAE1F,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,gEAAgE;AAChE,MAAM,OAAO,UAAU;IACZ,MAAM,CAA0B;IAChC,KAAK,CAA0B;IAC/B,KAAK,CAAU;IACf,KAAK,CAA0B;IAC/B,KAAK,CAAU;IACf,QAAQ,CAAU;IAClB,MAAM,CAAU;
|
|
1
|
+
{"version":3,"file":"Invocation.js","sourceRoot":"","sources":["../../src/wire/Invocation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InvocationContext.d.ts","sourceRoot":"","sources":["../../src/wire/InvocationContext.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"InvocationContext.d.ts","sourceRoot":"","sources":["../../src/wire/InvocationContext.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignedCall.d.ts","sourceRoot":"","sources":["../../src/wire/SignedCall.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,eAAO,MAAM,cAAc,QAAc,CAAC;AAE1C,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"SignedCall.d.ts","sourceRoot":"","sources":["../../src/wire/SignedCall.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,eAAO,MAAM,cAAc,QAAc,CAAC;AAE1C,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-alpha.0",
|
|
4
4
|
"description": "The core — scan, call contract, façades, remote stand-ins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@typescript/typescript6": "^6.0.2",
|
|
63
63
|
"dequal": "^2.0.3",
|
|
64
|
-
"@fougere/
|
|
65
|
-
"@fougere/
|
|
64
|
+
"@fougere/schema": "0.11.0-alpha.0",
|
|
65
|
+
"@fougere/container": "0.11.0-alpha.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^22.0.0",
|
|
69
|
-
"@fougere/compiler": "0.
|
|
69
|
+
"@fougere/compiler": "0.11.0-alpha.0"
|
|
70
70
|
},
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"access": "public"
|
package/src/CallIdentity.ts
CHANGED
|
@@ -31,6 +31,8 @@ async function boundTo(call: SignedCall) {
|
|
|
31
31
|
params: call.params ?? {},
|
|
32
32
|
query: call.query ?? {},
|
|
33
33
|
input: await digestOf(call.input),
|
|
34
|
+
// Omitted when absent, so a call with no hour pins what it always pinned.
|
|
35
|
+
...(call.runAt === undefined ? {} : { runAt: call.runAt }),
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
|
package/src/Constraint.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** A promise the judge at the facade cannot keep, because it is about rows it never sees. */
|
|
2
|
-
export type Constraint = 'unique';
|
|
2
|
+
export type Constraint = 'unique' | 'relation';
|
package/src/Source.ts
CHANGED
|
@@ -6,10 +6,11 @@ import type { SourceConfig } from './SourceConfig.js';
|
|
|
6
6
|
|
|
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
11
|
if ((schema.getUnique() ?? []).length > 0) return true;
|
|
11
12
|
|
|
12
|
-
return
|
|
13
|
+
return fields.some((field) => Role.of(field).isUnique);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SchemaView } from '@fougere/schema';
|
|
2
|
+
import type { Constraint } from '../Constraint.js';
|
|
3
|
+
import type { Peer } from './Peer.js';
|
|
4
|
+
import type { Journal } from '../dispatch/Journal.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where an entity's rows are hosted, and what the host keeps — what a `ref()` asks the boot.
|
|
8
|
+
*
|
|
9
|
+
* Documented: [entities](https://fougere.dev/docs/schema/entities).
|
|
10
|
+
*/
|
|
11
|
+
export interface Hosting {
|
|
12
|
+
/** False behind `remotes:` — whose `sourceOf` still answers the default, and means nothing. */
|
|
13
|
+
hostedHere(entity: string): boolean;
|
|
14
|
+
sourceOf(entity: string): string;
|
|
15
|
+
enforces(source: string, constraint: Constraint): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Resolved through the frond that owns it, never the asking frond's scope: a scope sees its
|
|
18
|
+
* parent and not its siblings, so a target of another frond was answered by nothing.
|
|
19
|
+
*/
|
|
20
|
+
storageOf(entity: string): unknown | undefined;
|
|
21
|
+
/** Every entity of every frond — a row is named from wherever its namer was declared. */
|
|
22
|
+
entities(): ReadonlyMap<string, SchemaView>;
|
|
23
|
+
/**
|
|
24
|
+
* The process that HOLDS an entity's rows, when this one does not. Absent when nothing can
|
|
25
|
+
* reach it: no `remotes:` entry, or no transport to carry the ask.
|
|
26
|
+
*/
|
|
27
|
+
peerOf(entity: string): Peer | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* One per declared remote, whatever this process knows of what lives there.
|
|
30
|
+
*
|
|
31
|
+
* A frond behind `remotes:` may have no sources here at all — `fougere sync` writes a card
|
|
32
|
+
* and nothing else — so this process cannot see that its rows name ours. Only the process
|
|
33
|
+
* holding them can, which is why a release asks every remote rather than deciding for it.
|
|
34
|
+
*/
|
|
35
|
+
peers(): readonly Peer[];
|
|
36
|
+
/** The journal a package registered, resolved per call — absent for what CARRIES one. */
|
|
37
|
+
journal(entity: string): Journal | undefined;
|
|
38
|
+
}
|
package/src/boot/Peer.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The process that holds rows this one names, reached by the two readings core always serves. */
|
|
2
|
+
export interface Peer {
|
|
3
|
+
/** Which of these keys no row answers over there — the write's question, asked across. */
|
|
4
|
+
missing(entity: string, keys: readonly unknown[]): Promise<readonly unknown[]>;
|
|
5
|
+
/** Who names these rows over there, and what becomes of them — its own `dependentsOf`. */
|
|
6
|
+
dependents(entity: string): Promise<unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Release a row over there: its rows go first, then the row, on its side of the wire.
|
|
9
|
+
*
|
|
10
|
+
* `visited` travels so a release cannot bounce: two processes that declare each other would
|
|
11
|
+
* otherwise ask each other about the same row forever. It is carried rather than held,
|
|
12
|
+
* because the two sides share no memory.
|
|
13
|
+
*/
|
|
14
|
+
release(entity: string, key: unknown, visited: readonly string[]): Promise<void>;
|
|
15
|
+
}
|
package/src/boot/bootstrap.ts
CHANGED
|
@@ -5,6 +5,12 @@ import type { HandlerEntry } from '../descriptor/HandlerEntry.js';
|
|
|
5
5
|
import type { PresenterEntry } from '../descriptor/PresenterEntry.js';
|
|
6
6
|
import { hostedBy } from './hosted.js';
|
|
7
7
|
import { installFrond, type Assembly } from './install.js';
|
|
8
|
+
import { dependentsOf, releasing, unfinishable, unheldAmong } from './relations.js';
|
|
9
|
+
import { release as releaseRow } from '../dispatch/Release.js';
|
|
10
|
+
import type { Hosting } from './Hosting.js';
|
|
11
|
+
import { peerOver } from './peerOver.js';
|
|
12
|
+
import { JOURNAL, type Journal } from '../dispatch/Journal.js';
|
|
13
|
+
import type { RelationCheck } from '../dispatch/RelationCheck.js';
|
|
8
14
|
import { refusalOf, type Diagnostic } from '../diagnostic.js';
|
|
9
15
|
import type { AuthRuntime } from './AuthRuntime.js';
|
|
10
16
|
import type { App } from './App.js';
|
|
@@ -30,7 +36,8 @@ import { type EffectiveOperationsMap } from '../EffectiveOperationsMap.js';
|
|
|
30
36
|
|
|
31
37
|
import { InFlight } from '../dispatch/InFlight.js';
|
|
32
38
|
// The keys, each read from where its concept is declared — never respelled here.
|
|
33
|
-
import { type RpcAnswer } from '../wire/RpcAnswer.js';
|
|
39
|
+
import { RPC_ENTITY, type RpcAnswer } from '../wire/RpcAnswer.js';
|
|
40
|
+
import { Invocation } from '../wire/Invocation.js';
|
|
34
41
|
import { facadeKeyOf } from '../wire/Facade.js';
|
|
35
42
|
import { identityCardOf } from './card.js';
|
|
36
43
|
import { AppLifecycle, migrating } from './AppLifecycle.js';
|
|
@@ -251,12 +258,16 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
251
258
|
error,
|
|
252
259
|
),
|
|
253
260
|
);
|
|
254
|
-
|
|
261
|
+
// Resolved at the call and never here: a journal is a provider of a brought frond, and
|
|
262
|
+
// the extension that registers it rises long after this line.
|
|
263
|
+
const keeping = (): Journal | undefined => journalOf();
|
|
264
|
+
const dispatcher = new Dispatcher(routeRegistry, inflight, dispatchLifecycle, undefined, keeping);
|
|
255
265
|
const localDispatcher = new Dispatcher(
|
|
256
266
|
routeRegistry,
|
|
257
267
|
inflight,
|
|
258
268
|
dispatchLifecycle,
|
|
259
269
|
new LocalRoutePolicy((surface) => fronds.servedNames(surface)),
|
|
270
|
+
keeping,
|
|
260
271
|
);
|
|
261
272
|
|
|
262
273
|
function getMiddlewares(entity: string): AppMiddleware[] {
|
|
@@ -280,6 +291,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
280
291
|
|
|
281
292
|
/** What every check of this boot writes into — refused together, once they have all run. */
|
|
282
293
|
const refused: Diagnostic[] = [];
|
|
294
|
+
const relations: RelationCheck[] = [];
|
|
283
295
|
keyClaims(fronds, options.remotes, refused);
|
|
284
296
|
// Said before anything is installed: a key claimed twice makes every later error worse —
|
|
285
297
|
// the route registry collides first, and names a route instead of the two fronds.
|
|
@@ -313,6 +325,78 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
313
325
|
[...operations].map(([name, operation]) => [name, operation as OperationContract] as const),
|
|
314
326
|
);
|
|
315
327
|
|
|
328
|
+
/**
|
|
329
|
+
* The storage an entity is backed by — the dual of `facadeFor`, which serves its client-facing
|
|
330
|
+
* facade.
|
|
331
|
+
*/
|
|
332
|
+
const storageFor = (entity: string): unknown | undefined => {
|
|
333
|
+
const owner = fronds.owner(entity);
|
|
334
|
+
if (!owner) return undefined;
|
|
335
|
+
|
|
336
|
+
const key = storageKeyOf(entity);
|
|
337
|
+
try {
|
|
338
|
+
return container.resolve<Container>(`frond:${owner.name}`).resolve(key);
|
|
339
|
+
} catch {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// What carries a release writes none: an instrumentation frond's own rows are kept while a
|
|
345
|
+
// release happens, and journalling them would begin one inside the one being written down.
|
|
346
|
+
const carriesRelease = new Set(
|
|
347
|
+
fronds.filter((one) => one.brought).flatMap((one) => one.entities.map((e) => e.name)),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The journal a package registered, found where its own frond put it — a provider lands in
|
|
352
|
+
* its frond's scope, and the app container sees none of them.
|
|
353
|
+
*/
|
|
354
|
+
const journalOf = (): Journal | undefined => {
|
|
355
|
+
for (const frond of fronds) {
|
|
356
|
+
if (!frond.brought) continue;
|
|
357
|
+
const scope = container.resolve<Container>(`frond:${frond.name}`);
|
|
358
|
+
if (scope.has(JOURNAL)) return scope.resolve<Journal>(JOURNAL);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return undefined;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const hosting: Hosting = {
|
|
365
|
+
hostedHere: (entity) => !(options.remotes && (frondOf.get(entity) ?? '') in options.remotes),
|
|
366
|
+
sourceOf: (name) => options.sourceOf?.(name) ?? 'db',
|
|
367
|
+
enforces: (source, constraint) => options.enforces?.(source, constraint) ?? false,
|
|
368
|
+
storageOf: storageFor,
|
|
369
|
+
entities: () => entityByName,
|
|
370
|
+
// The two readings core serves in EVERY process, asked of the one that holds the rows.
|
|
371
|
+
// Nothing is cached: a peer that was down at boot answers the next call.
|
|
372
|
+
// Built from `remotes:` itself, not from the router: the router indexes by ENTITY, read
|
|
373
|
+
// off a card, and the question here is asked of a PROCESS about rows it may be alone
|
|
374
|
+
// in knowing about.
|
|
375
|
+
// Resolved per call and never at boot: the package that registers it rises in the
|
|
376
|
+
// ascent, long after this answer is built.
|
|
377
|
+
journal: (entity) => (carriesRelease.has(entity) ? undefined : journalOf()),
|
|
378
|
+
peers: () => (options.remoteTransport
|
|
379
|
+
? declaredRemotes.map(([, url]) => peerOver(options.remoteTransport!(url)))
|
|
380
|
+
: []),
|
|
381
|
+
peerOf: (entity) => (remoteRouter ? {
|
|
382
|
+
dependents: async (named) => (await remoteRouter.route(entity))
|
|
383
|
+
.transport({ entity: RPC_ENTITY, op: 'dependents' },
|
|
384
|
+
{ ...Invocation.empty, params: { entity: named } as never }),
|
|
385
|
+
missing: async (named, keys) => {
|
|
386
|
+
const answer = await (await remoteRouter.route(entity))
|
|
387
|
+
.transport({ entity: RPC_ENTITY, op: 'holds' },
|
|
388
|
+
{ ...Invocation.empty, params: { entity: named, keys } as never });
|
|
389
|
+
|
|
390
|
+
return (answer as { missing: readonly unknown[] }).missing;
|
|
391
|
+
},
|
|
392
|
+
release: async (named, key) => {
|
|
393
|
+
const route = await remoteRouter.route(entity);
|
|
394
|
+
await route.transport({ entity: RPC_ENTITY, op: 'release' },
|
|
395
|
+
{ ...Invocation.empty, params: { entity: named, key } as never });
|
|
396
|
+
},
|
|
397
|
+
} : undefined),
|
|
398
|
+
};
|
|
399
|
+
|
|
316
400
|
// Every port an implementation was bound to, so a `ports:` entry that named none
|
|
317
401
|
// can say so rather than look obeyed.
|
|
318
402
|
const boundPorts = new Set<string>();
|
|
@@ -322,11 +406,34 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
322
406
|
// table, one emission list — so what a frond serves is there for the next one to find.
|
|
323
407
|
const assembly: Assembly = {
|
|
324
408
|
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
325
|
-
boundPorts, refused, operationModel, entityByName, frondOf, contractsOf,
|
|
326
|
-
log, options,
|
|
409
|
+
boundPorts, refused, relations, hosting, operationModel, entityByName, frondOf, contractsOf,
|
|
410
|
+
getMiddlewares, use, log, options,
|
|
327
411
|
};
|
|
328
412
|
for (const frond of fronds) await installFrond(frond, assembly);
|
|
329
413
|
|
|
414
|
+
// A reference no key holds and no storage answers: said at the END, for the reason the
|
|
415
|
+
// `ports:` warning below is — a target is another frond's, so no single frond can tell
|
|
416
|
+
// an absence from a neighbour that had not been installed yet.
|
|
417
|
+
const unheld = unheldAmong(relations, hosting);
|
|
418
|
+
// Declared here, and this process cannot finish what it starts — both halves are local,
|
|
419
|
+
// so nothing is asked of anyone. Said once, after every frond installed, because a journal
|
|
420
|
+
// arrives with an extension and an extension rises last.
|
|
421
|
+
const unfinished = unfinishable(hosting);
|
|
422
|
+
if (unfinished.length > 0) {
|
|
423
|
+
log.warn(
|
|
424
|
+
`[relations] ${unfinished.join(', ')} — declared, and this process keeps nothing on `
|
|
425
|
+
+ 'restart: a release interrupted here is not resumed. Install @fougere/workflow, or '
|
|
426
|
+
+ 'expect to finish one by hand.',
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (unheld.length > 0) {
|
|
431
|
+
log.warn(
|
|
432
|
+
`[relations] ${unheld.join(', ')} — declared, and nothing in this process holds them: `
|
|
433
|
+
+ 'no foreign key, and the target answers elsewhere. A row may name one that is gone.',
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
330
437
|
// Every check of the install, said at once — a boot that stops at the first makes the
|
|
331
438
|
// next one visible only after a fix and a restart.
|
|
332
439
|
const bootRefusal = refusalOf(refused, 'declaration(s) that do not hold');
|
|
@@ -495,22 +602,6 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
495
602
|
: undefined;
|
|
496
603
|
};
|
|
497
604
|
|
|
498
|
-
/**
|
|
499
|
-
* The storage an entity is backed by — the dual of `facadeFor`, which serves its client-facing
|
|
500
|
-
* facade.
|
|
501
|
-
*/
|
|
502
|
-
const storageFor = (entity: string): unknown | undefined => {
|
|
503
|
-
const owner = fronds.owner(entity);
|
|
504
|
-
if (!owner) return undefined;
|
|
505
|
-
|
|
506
|
-
const key = storageKeyOf(entity);
|
|
507
|
-
try {
|
|
508
|
-
return container.resolve<Container>(`frond:${owner.name}`).resolve(key);
|
|
509
|
-
} catch {
|
|
510
|
-
return undefined;
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
605
|
/** The presenter of an entity, resolved through its owning frond's scope. */
|
|
515
606
|
const presenterFor = (entity: string): unknown | undefined => {
|
|
516
607
|
const owner = fronds.owner(entity);
|
|
@@ -577,8 +668,39 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
577
668
|
|
|
578
669
|
// The card is an rpc op like any other, so one registry answers and one refusal names
|
|
579
670
|
// what is served. A package's op is declared the same way, from outside.
|
|
671
|
+
// What a release is, resolved once and registered: a package that drives one asks for it
|
|
672
|
+
// by type, the way every other dependency is asked for.
|
|
673
|
+
container.registerValue('Releasing', releasing(hosting));
|
|
674
|
+
|
|
580
675
|
app.serveRpc('discover', (_invocation, surface) => identityCardOf(app, surface));
|
|
581
676
|
|
|
677
|
+
// What a release asks another process, and what it tells it to do. Served by EVERY process
|
|
678
|
+
// the way `discover` is: a frond that hosts rows is the only one that can answer for them.
|
|
679
|
+
//
|
|
680
|
+
// Read off the STORAGE and never off a facade: a facade answers what its handler chose to
|
|
681
|
+
// show — `PostHandler.list` hands back published posts — so a draft naming a deleted user
|
|
682
|
+
// would be invisible to the very question that exists to find it.
|
|
683
|
+
// The dual of `release`, and the same reason it is served by every process: only the one
|
|
684
|
+
// that holds the rows can say a key is missing, and only from the STORAGE — a facade
|
|
685
|
+
// answers what its handler chose to show.
|
|
686
|
+
app.serveRpc('holds', async (invocation) => {
|
|
687
|
+
const named = String(invocation.params.entity);
|
|
688
|
+
const keys = (invocation.params.keys ?? []) as readonly unknown[];
|
|
689
|
+
const rows = storageFor(named) as { findByKeys(k: readonly string[]): Promise<Map<string, unknown>> } | undefined;
|
|
690
|
+
if (!rows) return { missing: [] };
|
|
691
|
+
const found = await rows.findByKeys(keys.map(String));
|
|
692
|
+
|
|
693
|
+
return { missing: keys.filter((key) => !found.has(String(key))) };
|
|
694
|
+
});
|
|
695
|
+
app.serveRpc('dependents', (invocation) =>
|
|
696
|
+
dependentsOf(String(invocation.params.entity), hosting));
|
|
697
|
+
app.serveRpc('release', async (invocation) => {
|
|
698
|
+
await releaseRow(String(invocation.params.entity), invocation.params.key, releasing(hosting),
|
|
699
|
+
(invocation.params.visited ?? []) as readonly string[]);
|
|
700
|
+
|
|
701
|
+
return { released: true };
|
|
702
|
+
});
|
|
703
|
+
|
|
582
704
|
// The last thing the boot does, and the first thing a release undoes. An extension may
|
|
583
705
|
// await here — which is what a provider needing to OPEN something could never do.
|
|
584
706
|
built = app;
|
package/src/boot/declared.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
|
|
7
7
|
import { facadeKeyOf } from '../wire/Facade.js';
|
|
8
|
+
import { factsAnnouncedBy, factsAwaitedBy, factsListenedTo } from '../wire/Emit.js';
|
|
8
9
|
import { type DeclaredEdge } from '../wire/topology/DeclaredEdge.js';
|
|
9
10
|
import { type DeclaredFrond } from '../wire/topology/DeclaredFrond.js';
|
|
10
11
|
import { type DeclaredTopology } from '../wire/topology/DeclaredTopology.js';
|
|
@@ -65,21 +66,52 @@ export function reachedBy(deps: readonly string[], from: string, index: Map<stri
|
|
|
65
66
|
return [...new Set(reached)];
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The two ways a call leaves a frond: a façade its code names, and a fact it announces.
|
|
71
|
+
*
|
|
72
|
+
* The second cannot be read the way the first is. A dependency names what it reaches, where an
|
|
73
|
+
* announcement names a SUBJECT and a subscriber names nothing at all — so the edge is found from
|
|
74
|
+
* both ends, the announcer's deps and the listener's binding plan. Reading the deps alone
|
|
75
|
+
* reported `demos/pipe-split` as four fronds with nothing between them, and three of its four
|
|
76
|
+
* fronds are reached by a fact.
|
|
77
|
+
*/
|
|
68
78
|
function crossings(hosted: readonly FrondDescriptor[]): DeclaredEdge[] {
|
|
69
79
|
const index = servedBy(hosted);
|
|
80
|
+
const heard = listenedBy(hosted);
|
|
70
81
|
const found = new Map<string, DeclaredEdge>();
|
|
82
|
+
const cross = (from: string, to: string) => {
|
|
83
|
+
if (from !== to) found.set(`${from} ${to}`, { from, to });
|
|
84
|
+
};
|
|
71
85
|
|
|
72
86
|
for (const frond of hosted) {
|
|
73
87
|
for (const handler of frond.handlers) {
|
|
74
|
-
for (const to of reachedBy(handler.deps, frond.name, index))
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
for (const to of reachedBy(handler.deps, frond.name, index)) cross(frond.name, to);
|
|
89
|
+
}
|
|
90
|
+
for (const fact of announcedIn(frond)) {
|
|
91
|
+
for (const to of heard.get(fact) ?? []) cross(frond.name, to);
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
return [...found.values()];
|
|
81
96
|
}
|
|
82
97
|
|
|
98
|
+
/** Which fronds listen to each fact — every one of them, since an announcement names no recipient. */
|
|
99
|
+
function listenedBy(hosted: readonly FrondDescriptor[]): Map<string, string[]> {
|
|
100
|
+
const index = new Map<string, string[]>();
|
|
101
|
+
for (const frond of hosted) {
|
|
102
|
+
for (const fact of factsListenedTo(frond.handlers)) {
|
|
103
|
+
index.set(fact, [...(index.get(fact) ?? []), frond.name]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A fact announced here, whether the announcer waits for an answer or not. */
|
|
111
|
+
function announcedIn(frond: FrondDescriptor): string[] {
|
|
112
|
+
return [...new Set([...factsAnnouncedBy(frond.handlers), ...factsAwaitedBy(frond.handlers)])];
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
function hostOf(address: string): string {
|
|
84
116
|
try {
|
|
85
117
|
const url = new URL(address);
|
package/src/boot/install.ts
CHANGED
|
@@ -26,6 +26,9 @@ import { inheritsCrud, subjectOf } from '../prefab/CrudConstructor.js';
|
|
|
26
26
|
import { repositoryKeyOf } from '../prefab/RepositoryConstructor.js';
|
|
27
27
|
import { storageKeyOf } from '../storage/Storage.js';
|
|
28
28
|
import { declares } from '../Source.js';
|
|
29
|
+
import { heldBy, refuseUnwritableNull, releasing } from './relations.js';
|
|
30
|
+
import type { Hosting } from './Hosting.js';
|
|
31
|
+
import type { RelationCheck } from '../dispatch/RelationCheck.js';
|
|
29
32
|
import { presenterKeyOf } from '../prefab/presenter.js';
|
|
30
33
|
import { collectorKeyOf } from '../prefab/collector.js';
|
|
31
34
|
import { RouteAddress } from '../wire/RouteAddress.js';
|
|
@@ -51,6 +54,14 @@ export interface Assembly {
|
|
|
51
54
|
boundPorts: Set<string>;
|
|
52
55
|
/** Where a check writes what does not hold — collected across fronds, refused together. */
|
|
53
56
|
refused: Diagnostic[];
|
|
57
|
+
/**
|
|
58
|
+
* The references this process reads itself, collected across fronds. Whether one is
|
|
59
|
+
* ANSWERED can only be asked once every frond has registered, which is why the list
|
|
60
|
+
* leaves here rather than being judged where it is built.
|
|
61
|
+
*/
|
|
62
|
+
relations: RelationCheck[];
|
|
63
|
+
/** Where each entity is hosted — one answer for a frond's storage, its frames and its surfaces. */
|
|
64
|
+
hosting: Hosting;
|
|
54
65
|
/** What the model resolved before the boot performed any side effect. */
|
|
55
66
|
operationModel: EffectiveOperationModel;
|
|
56
67
|
entityByName: Map<string, SchemaView>;
|
|
@@ -87,6 +98,9 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
87
98
|
}
|
|
88
99
|
const scope = container.createScope();
|
|
89
100
|
const frondLog = log.child(frond.name);
|
|
101
|
+
// The frond's own voice, a child of the APP logger and never of `log` — which is the
|
|
102
|
+
// boot's, so a service's line would have claimed `boot:` long after the boot was over.
|
|
103
|
+
scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
|
|
90
104
|
|
|
91
105
|
// `reads:` is what makes a cross-source reader exist here, and the list IS its
|
|
92
106
|
// environment — a source holding none of these is never opened. Registered under
|
|
@@ -174,6 +188,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
174
188
|
// When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
|
|
175
189
|
if (options.storageFactory) {
|
|
176
190
|
const unenforced: string[] = [];
|
|
191
|
+
const release = releasing(assembly.hosting);
|
|
177
192
|
for (const entity of frond.entities) {
|
|
178
193
|
const key = storageKeyOf(entity.name);
|
|
179
194
|
const source = options.sourceOf?.(entity.name) ?? 'db';
|
|
@@ -194,7 +209,12 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
194
209
|
// arrived. Same order the client facade has held since `StorageGuard` existed.
|
|
195
210
|
const linked = wrapping('Storage', seams.get('Storage') ?? [], scoped, (dep) => scope.resolve(dep));
|
|
196
211
|
// Storage is a way out like the client surface — see `StorageGuard`.
|
|
197
|
-
|
|
212
|
+
refused.push(...refuseUnwritableNull(entity.entityClass, entity.name, entity.filePath));
|
|
213
|
+
const relations = heldBy(entity.entityClass, entity.name, assembly.hosting);
|
|
214
|
+
assembly.relations.push(...relations);
|
|
215
|
+
const guarded = new StorageGuard(
|
|
216
|
+
entity.entityClass.getFields(), entity.name, {}, relations, release,
|
|
217
|
+
).guard(linked);
|
|
198
218
|
scope.registerValue(key, guarded);
|
|
199
219
|
|
|
200
220
|
// The default repository IS the guarded port — it already answers every gesture a
|
|
@@ -243,6 +263,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
243
263
|
sourceOf: options.sourceOf,
|
|
244
264
|
transacts: options.transacts,
|
|
245
265
|
transacted: options.transacted,
|
|
266
|
+
hosting: assembly.hosting,
|
|
246
267
|
log: frondLog,
|
|
247
268
|
},
|
|
248
269
|
refused,
|
|
@@ -415,7 +436,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
415
436
|
? { view: (outputSchema as { getFields(): Fields }).getFields() }
|
|
416
437
|
: {}),
|
|
417
438
|
outOfView: (message) => frondLog.warn(message),
|
|
418
|
-
}).guard(scoped);
|
|
439
|
+
}, heldBy(entity.entityClass, entity.name, assembly.hosting), releasing(assembly.hosting)).guard(scoped);
|
|
419
440
|
surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
|
|
420
441
|
surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
|
|
421
442
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The two readings core serves everywhere, asked of one process over one transport. */
|
|
2
|
+
import { Invocation } from '../wire/Invocation.js';
|
|
3
|
+
import { RPC_ENTITY } from '../wire/RpcAnswer.js';
|
|
4
|
+
import type { Transport } from '../wire/Transport.js';
|
|
5
|
+
import type { Peer } from './Peer.js';
|
|
6
|
+
|
|
7
|
+
export function peerOver(transport: Transport): Peer {
|
|
8
|
+
const ask = (op: string, params: Record<string, unknown>) =>
|
|
9
|
+
transport({ entity: RPC_ENTITY, op }, { ...Invocation.empty, params: params as never });
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
missing: async (entity, keys) =>
|
|
13
|
+
((await ask('holds', { entity, keys })) as { missing: readonly unknown[] }).missing,
|
|
14
|
+
dependents: (entity) => ask('dependents', { entity }),
|
|
15
|
+
release: async (entity, key, visited) => { await ask('release', { entity, key, visited }); },
|
|
16
|
+
};
|
|
17
|
+
}
|