@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.
Files changed (105) hide show
  1. package/dist/CallIdentity.d.ts.map +1 -1
  2. package/dist/CallIdentity.js +2 -0
  3. package/dist/CallIdentity.js.map +1 -1
  4. package/dist/Constraint.d.ts +1 -1
  5. package/dist/Constraint.d.ts.map +1 -1
  6. package/dist/Source.d.ts.map +1 -1
  7. package/dist/Source.js +4 -3
  8. package/dist/Source.js.map +1 -1
  9. package/dist/boot/Hosting.d.ts +38 -0
  10. package/dist/boot/Hosting.d.ts.map +1 -0
  11. package/dist/boot/Hosting.js +2 -0
  12. package/dist/boot/Hosting.js.map +1 -0
  13. package/dist/boot/Peer.d.ts +16 -0
  14. package/dist/boot/Peer.d.ts.map +1 -0
  15. package/dist/boot/Peer.js +2 -0
  16. package/dist/boot/Peer.js.map +1 -0
  17. package/dist/boot/bootstrap.d.ts.map +1 -1
  18. package/dist/boot/bootstrap.js +122 -20
  19. package/dist/boot/bootstrap.js.map +1 -1
  20. package/dist/boot/declared.d.ts.map +1 -1
  21. package/dist/boot/declared.js +35 -3
  22. package/dist/boot/declared.js.map +1 -1
  23. package/dist/boot/install.d.ts +10 -0
  24. package/dist/boot/install.d.ts.map +1 -1
  25. package/dist/boot/install.js +11 -2
  26. package/dist/boot/install.js.map +1 -1
  27. package/dist/boot/peerOver.d.ts +4 -0
  28. package/dist/boot/peerOver.d.ts.map +1 -0
  29. package/dist/boot/peerOver.js +12 -0
  30. package/dist/boot/peerOver.js.map +1 -0
  31. package/dist/boot/relations.d.ts +54 -0
  32. package/dist/boot/relations.d.ts.map +1 -0
  33. package/dist/boot/relations.js +150 -0
  34. package/dist/boot/relations.js.map +1 -0
  35. package/dist/boot/together.d.ts +3 -0
  36. package/dist/boot/together.d.ts.map +1 -1
  37. package/dist/boot/together.js +2 -1
  38. package/dist/boot/together.js.map +1 -1
  39. package/dist/contract.d.ts +1 -0
  40. package/dist/contract.d.ts.map +1 -1
  41. package/dist/contract.js.map +1 -1
  42. package/dist/dispatch/Dependent.d.ts +19 -0
  43. package/dist/dispatch/Dependent.d.ts.map +1 -0
  44. package/dist/dispatch/Dependent.js +2 -0
  45. package/dist/dispatch/Dependent.js.map +1 -0
  46. package/dist/dispatch/Dispatcher.d.ts +14 -1
  47. package/dist/dispatch/Dispatcher.d.ts.map +1 -1
  48. package/dist/dispatch/Dispatcher.js +36 -8
  49. package/dist/dispatch/Dispatcher.js.map +1 -1
  50. package/dist/dispatch/Journal.d.ts +25 -0
  51. package/dist/dispatch/Journal.d.ts.map +1 -0
  52. package/dist/dispatch/Journal.js +3 -0
  53. package/dist/dispatch/Journal.js.map +1 -0
  54. package/dist/dispatch/RelationCheck.d.ts +22 -0
  55. package/dist/dispatch/RelationCheck.d.ts.map +1 -0
  56. package/dist/dispatch/RelationCheck.js +2 -0
  57. package/dist/dispatch/RelationCheck.js.map +1 -0
  58. package/dist/dispatch/Release.d.ts +36 -0
  59. package/dist/dispatch/Release.d.ts.map +1 -0
  60. package/dist/dispatch/Release.js +124 -0
  61. package/dist/dispatch/Release.js.map +1 -0
  62. package/dist/dispatch/StorageGuard.d.ts +18 -1
  63. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  64. package/dist/dispatch/StorageGuard.js +57 -1
  65. package/dist/dispatch/StorageGuard.js.map +1 -1
  66. package/dist/index.d.ts +5 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +3 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/wire/Emit.d.ts +9 -0
  71. package/dist/wire/Emit.d.ts.map +1 -1
  72. package/dist/wire/Emit.js +16 -0
  73. package/dist/wire/Emit.js.map +1 -1
  74. package/dist/wire/Invocation.d.ts +2 -1
  75. package/dist/wire/Invocation.d.ts.map +1 -1
  76. package/dist/wire/Invocation.js +4 -1
  77. package/dist/wire/Invocation.js.map +1 -1
  78. package/dist/wire/InvocationContext.d.ts +2 -0
  79. package/dist/wire/InvocationContext.d.ts.map +1 -1
  80. package/dist/wire/SignedCall.d.ts +2 -0
  81. package/dist/wire/SignedCall.d.ts.map +1 -1
  82. package/package.json +4 -4
  83. package/src/CallIdentity.ts +2 -0
  84. package/src/Constraint.ts +1 -1
  85. package/src/Source.ts +3 -2
  86. package/src/boot/Hosting.ts +38 -0
  87. package/src/boot/Peer.ts +15 -0
  88. package/src/boot/bootstrap.ts +142 -20
  89. package/src/boot/declared.ts +35 -3
  90. package/src/boot/install.ts +23 -2
  91. package/src/boot/peerOver.ts +17 -0
  92. package/src/boot/relations.ts +172 -0
  93. package/src/boot/together.ts +5 -1
  94. package/src/contract.ts +1 -0
  95. package/src/dispatch/Dependent.ts +19 -0
  96. package/src/dispatch/Dispatcher.ts +43 -7
  97. package/src/dispatch/Journal.ts +26 -0
  98. package/src/dispatch/RelationCheck.ts +21 -0
  99. package/src/dispatch/Release.ts +156 -0
  100. package/src/dispatch/StorageGuard.ts +62 -0
  101. package/src/index.ts +5 -0
  102. package/src/wire/Emit.ts +23 -0
  103. package/src/wire/Invocation.ts +3 -1
  104. package/src/wire/InvocationContext.ts +2 -0
  105. 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)
@@ -1 +1 @@
1
- {"version":3,"file":"Emit.js","sourceRoot":"","sources":["../../src/wire/Emit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAU7C,+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,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"}
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
- /** Gives every entry and every transport the same seven-member value, frozen. */
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;IAEzB,iFAAiF;IACjF,OAAO;IAaP,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"}
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"}
@@ -29,7 +29,8 @@ export class Invocation {
29
29
  trace;
30
30
  identity;
31
31
  caller;
32
- /** Gives every entry and every transport the same seven-member value, frozen. */
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":"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"}
1
+ {"version":3,"file":"Invocation.js","sourceRoot":"","sources":["../../src/wire/Invocation.ts"],"names":[],"mappings":"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"}
@@ -6,5 +6,7 @@ export interface InvocationContext {
6
6
  trace?: string;
7
7
  identity?: string;
8
8
  caller?: string;
9
+ /** Epoch milliseconds before which this call is not to run. */
10
+ runAt?: number;
9
11
  }
10
12
  //# sourceMappingURL=InvocationContext.d.ts.map
@@ -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;CACjB"}
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"}
@@ -8,5 +8,7 @@ export interface SignedCall {
8
8
  query?: Record<string, unknown>;
9
9
  input?: unknown;
10
10
  state?: Record<string, unknown>;
11
+ /** The hour, pinned like the input: moving it is moving the call. */
12
+ runAt?: number;
11
13
  }
12
14
  //# sourceMappingURL=SignedCall.d.ts.map
@@ -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;CACjC"}
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.10.0-alpha.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/container": "0.10.0-alpha.0",
65
- "@fougere/schema": "0.10.0-alpha.0"
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.10.0-alpha.0"
69
+ "@fougere/compiler": "0.11.0-alpha.0"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public"
@@ -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
- if (constraint !== 'unique') return false;
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 Object.values(schema.getFields()).some((field) => Role.of(field).isUnique);
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
+ }
@@ -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
+ }
@@ -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
- const dispatcher = new Dispatcher(routeRegistry, inflight, dispatchLifecycle);
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, getMiddlewares, use,
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;
@@ -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
- found.set(`${frond.name} ${to}`, { from: frond.name, to });
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);
@@ -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
- const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name).guard(linked);
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
+ }