@reventlessdev/trait-notification 1.0.0-alpha.5 → 1.0.0-alpha.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/trait-notification",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.7",
4
4
  "description": "Notification domain trait: rules, host contract, conformance suite and emitter for a per-recipient contact directory, a kind x channel subscription matrix, and the decision to send, suppress or record a message as undeliverable",
5
5
  "license": "Apache-2.0",
6
6
  "files": [
@@ -13,8 +13,8 @@
13
13
  "dependencies": {
14
14
  "sury": "11.0.0-rc.2",
15
15
  "sury-ppx": "11.0.0-rc.2",
16
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.203",
17
- "@reventlessdev/reventless-spec": "3.0.0-alpha.129"
16
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.205",
17
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.131"
18
18
  },
19
19
  "devDependencies": {
20
20
  "rescript": "12.3.0"
@@ -67,18 +67,30 @@ module type Binding = {
67
67
  module Spec: ReventlessGwt.Behavior_GWT.BehaviorSpec
68
68
  module Behavior: ReventlessGwt.Behavior_GWT.Behavior with module Spec = Spec
69
69
 
70
- /** History that brings the recipient into existence with nothing on file. */
71
- let created: array<Spec.consumedEvent>
72
-
73
- /** The competency's own facts, as the slice consumes them. */
74
- let announcedC: string => Spec.consumedEvent
75
- let subscribedC: (category, Notification_Rules.channel) => Spec.consumedEvent
76
- let unsubscribedC: (category, Notification_Rules.channel) => Spec.consumedEvent
77
- /** A source taken over, as the slice reads it back. Claims are per source and
78
- not per recipient, so a host produces these from wherever it keeps them —
79
- what this contract requires is only that the decision can see them. */
80
- let claimedC: (string, string) => Spec.consumedEvent
81
- let releasedC: string => Spec.consumedEvent
70
+ /** The same facts as the slice **consumes** them, for building history.
71
+
72
+ A slice declares `consumedEvent` and `event` separately and they differ —
73
+ the consumed form carries no recipient id, because the partition already
74
+ says whose it is. So a suite needs both spellings of the same fact: one to
75
+ lay down history, one to assert what was emitted.
76
+
77
+ Grouped rather than suffixed. The pairs used to read `announcedC` against
78
+ `announced`, which said "consumed" only to someone who already knew and
79
+ in a codebase where every binding also carries commands, a bare `C` reads
80
+ just as naturally as one. */
81
+ module Consumed: {
82
+ /** History that brings the recipient into existence with nothing on file. */
83
+ let created: array<Spec.consumedEvent>
84
+ let announced: string => Spec.consumedEvent
85
+ let subscribed: (category, Notification_Rules.channel) => Spec.consumedEvent
86
+ let unsubscribed: (category, Notification_Rules.channel) => Spec.consumedEvent
87
+ /** A source taken over, as the slice reads it back. Claims are per source
88
+ and not per recipient, so a host produces these from wherever it keeps
89
+ them — what this contract requires is only that the decision can see
90
+ them. */
91
+ let claimed: (string, string) => Spec.consumedEvent
92
+ let released: string => Spec.consumedEvent
93
+ }
82
94
 
83
95
  let announce: string => Spec.command
84
96
  let subscribe: (category, Notification_Rules.channel) => Spec.command
@@ -17,12 +17,12 @@ module Make = (B: Notification.Binding) => {
17
17
  module G = ReventlessGwt.Behavior_GWT.Make(B.Spec, B.Behavior)
18
18
  module R = Notification_Rules
19
19
 
20
- let announced = Array.concat(B.created, [B.announcedC(B.addressA)])
20
+ let announced = Array.concat(B.Consumed.created, [B.Consumed.announced(B.addressA)])
21
21
 
22
22
  let register = () =>
23
23
  G.describe(suiteName(B.Spec.name), () => {
24
24
  G.test("an announced contact is recorded", () =>
25
- G.givenEvents(B.created)->G.whenCmd(B.announce(B.addressA))->G.thenEvent(
25
+ G.givenEvents(B.Consumed.created)->G.whenCmd(B.announce(B.addressA))->G.thenEvent(
26
26
  B.announced(B.addressA),
27
27
  )
28
28
  )
@@ -44,7 +44,7 @@ module Make = (B: Notification.Binding) => {
44
44
  // A person is at the other end of this one, so they are told rather than
45
45
  // having a fact recorded about them.
46
46
  G.test("managing preferences for an unannounced recipient is refused", () =>
47
- G.givenEvents(B.created)
47
+ G.givenEvents(B.Consumed.created)
48
48
  ->G.whenCmd(B.subscribe(B.optional, B.announcedChannel))
49
49
  ->G.thenError(B.recipientUnknown)
50
50
  )
@@ -87,7 +87,7 @@ module Make = (B: Notification.Binding) => {
87
87
  )
88
88
 
89
89
  G.test("the address on the request is the one currently on file", () =>
90
- G.givenEvents(Array.concat(announced, [B.announcedC(B.addressB)]))
90
+ G.givenEvents(Array.concat(announced, [B.Consumed.announced(B.addressB)]))
91
91
  ->G.whenCmd(B.request(B.transactional, "ref-1"))
92
92
  ->G.thenEvent(B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB))
93
93
  )
@@ -100,7 +100,7 @@ module Make = (B: Notification.Binding) => {
100
100
 
101
101
  G.test("a recipient who opted out is suppressed", () =>
102
102
  G.givenEvents(
103
- Array.concat(announced, [B.unsubscribedC(B.transactional, B.announcedChannel)]),
103
+ Array.concat(announced, [B.Consumed.unsubscribed(B.transactional, B.announcedChannel)]),
104
104
  )
105
105
  ->G.whenCmd(B.request(B.transactional, "ref-3"))
106
106
  ->G.thenEvent(B.suppressed(B.transactional, "ref-3"))
@@ -108,7 +108,7 @@ module Make = (B: Notification.Binding) => {
108
108
 
109
109
  // The case the directory exists for.
110
110
  G.test("a request for a recipient nobody announced is undeliverable", () =>
111
- G.givenEvents(B.created)
111
+ G.givenEvents(B.Consumed.created)
112
112
  ->G.whenCmd(B.request(B.transactional, "ref-4"))
113
113
  ->G.thenEvent(B.undeliverable(B.transactional, "ref-4"))
114
114
  )
@@ -129,7 +129,7 @@ module Make = (B: Notification.Binding) => {
129
129
  )
130
130
 
131
131
  G.test("a default request for a claimed source is deferred", () =>
132
- G.givenEvents(Array.concat(announced, [B.claimedC("other:Source", "second-producer")]))
132
+ G.givenEvents(Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]))
133
133
  ->G.whenCmd(
134
134
  B.requestFrom(B.transactional, "ref-7", ~source="other:Source", ~origin=Default),
135
135
  )
@@ -140,7 +140,7 @@ module Make = (B: Notification.Binding) => {
140
140
  // keeps firing everywhere it was not taken over. Without this, a single
141
141
  // claim would silence the whole competency.
142
142
  G.test("a claim on one source leaves every other source alone", () =>
143
- G.givenEvents(Array.concat(announced, [B.claimedC("other:Source", "second-producer")]))
143
+ G.givenEvents(Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]))
144
144
  ->G.whenCmd(
145
145
  B.requestFrom(B.transactional, "ref-8", ~source=B.defaultSource, ~origin=Default),
146
146
  )
@@ -150,7 +150,7 @@ module Make = (B: Notification.Binding) => {
150
150
  // The producer that owns the source is the one that must get through, or
151
151
  // the handover would silence the entry rather than move it.
152
152
  G.test("a configured request for a claimed source goes through", () =>
153
- G.givenEvents(Array.concat(announced, [B.claimedC("other:Source", "second-producer")]))
153
+ G.givenEvents(Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]))
154
154
  ->G.whenCmd(
155
155
  B.requestFrom(B.transactional, "ref-9", ~source="other:Source", ~origin=Configured),
156
156
  )
@@ -163,7 +163,7 @@ module Make = (B: Notification.Binding) => {
163
163
  G.givenEvents(
164
164
  Array.concat(
165
165
  announced,
166
- [B.claimedC("other:Source", "second-producer"), B.releasedC("other:Source")],
166
+ [B.Consumed.claimed("other:Source", "second-producer"), B.Consumed.released("other:Source")],
167
167
  ),
168
168
  )
169
169
  ->G.whenCmd(
@@ -183,8 +183,8 @@ module Make = (B: Notification.Binding) => {
183
183
  Array.concat(
184
184
  announced,
185
185
  [
186
- B.subscribedC(B.transactional, channel),
187
- B.unsubscribedC(B.transactional, B.announcedChannel),
186
+ B.Consumed.subscribed(B.transactional, channel),
187
+ B.Consumed.unsubscribed(B.transactional, B.announcedChannel),
188
188
  ],
189
189
  ),
190
190
  )
@@ -13,34 +13,34 @@ function Make(B) {
13
13
  evolve: $$let.evolve,
14
14
  decide: $$let.decide
15
15
  });
16
- let announced = B.created.concat([B.announcedC(B.addressA)]);
16
+ let announced = B.Consumed.created.concat([B.Consumed.announced(B.addressA)]);
17
17
  let register = () => G.describe(suiteName(B.Spec.name), () => {
18
- G.test("an announced contact is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.announce(B.addressA)), B.announced(B.addressA)));
18
+ G.test("an announced contact is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(B.Consumed.created), B.announce(B.addressA)), B.announced(B.addressA)));
19
19
  G.test("re-announcing the address already on file is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(announced), B.announce(B.addressA))));
20
20
  G.test("a changed address is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.announce(B.addressB)), B.announced(B.addressB)));
21
- G.test("managing preferences for an unannounced recipient is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.created), B.subscribe(B.optional, B.announcedChannel)), B.recipientUnknown));
21
+ G.test("managing preferences for an unannounced recipient is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.Consumed.created), B.subscribe(B.optional, B.announcedChannel)), B.recipientUnknown));
22
22
  G.test("opting in to a kind that is off by default is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.subscribe(B.optional, B.announcedChannel)), B.subscribed(B.optional, B.announcedChannel)));
23
23
  G.test("subscribing to a kind already on by posture is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(announced), B.subscribe(B.transactional, B.announcedChannel))));
24
24
  G.test("opting out of a kind that is on is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.unsubscribe(B.transactional, B.announcedChannel)), B.unsubscribed(B.transactional, B.announcedChannel)));
25
25
  G.test("opting out of a kind already off is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(announced), B.unsubscribe(B.optional, B.announcedChannel))));
26
26
  G.test("a transactional request goes out with no explicit subscription", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.request(B.transactional, "ref-1")), B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressA)));
27
- G.test("the address on the request is the one currently on file", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.announcedC(B.addressB)])), B.request(B.transactional, "ref-1")), B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB)));
27
+ G.test("the address on the request is the one currently on file", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.announced(B.addressB)])), B.request(B.transactional, "ref-1")), B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB)));
28
28
  G.test("an optional request is suppressed with no explicit subscription", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.request(B.optional, "ref-2")), B.suppressed(B.optional, "ref-2")));
29
- G.test("a recipient who opted out is suppressed", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.unsubscribedC(B.transactional, B.announcedChannel)])), B.request(B.transactional, "ref-3")), B.suppressed(B.transactional, "ref-3")));
30
- G.test("a request for a recipient nobody announced is undeliverable", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.request(B.transactional, "ref-4")), B.undeliverable(B.transactional, "ref-4")));
29
+ G.test("a recipient who opted out is suppressed", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.unsubscribed(B.transactional, B.announcedChannel)])), B.request(B.transactional, "ref-3")), B.suppressed(B.transactional, "ref-3")));
30
+ G.test("a request for a recipient nobody announced is undeliverable", () => G.thenEvent(G.whenCmd(G.givenEvents(B.Consumed.created), B.request(B.transactional, "ref-4")), B.undeliverable(B.transactional, "ref-4")));
31
31
  G.test("with nothing claimed, a default request is decided as usual", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.requestFrom(B.transactional, "ref-6", B.defaultSource, "Default")), B.requested(B.transactional, "ref-6", B.announcedChannel, B.addressA)));
32
- G.test("a default request for a claimed source is deferred", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.claimedC("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-7", "other:Source", "Default")), B.deferred("ref-7", "other:Source")));
33
- G.test("a claim on one source leaves every other source alone", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.claimedC("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-8", B.defaultSource, "Default")), B.requested(B.transactional, "ref-8", B.announcedChannel, B.addressA)));
34
- G.test("a configured request for a claimed source goes through", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.claimedC("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-9", "other:Source", "Configured")), B.requestedConfigured(B.transactional, "ref-9", B.announcedChannel, B.addressA)));
32
+ G.test("a default request for a claimed source is deferred", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.claimed("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-7", "other:Source", "Default")), B.deferred("ref-7", "other:Source")));
33
+ G.test("a claim on one source leaves every other source alone", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.claimed("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-8", B.defaultSource, "Default")), B.requested(B.transactional, "ref-8", B.announcedChannel, B.addressA)));
34
+ G.test("a configured request for a claimed source goes through", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.claimed("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-9", "other:Source", "Configured")), B.requestedConfigured(B.transactional, "ref-9", B.announcedChannel, B.addressA)));
35
35
  G.test("a released source stops deferring", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([
36
- B.claimedC("other:Source", "second-producer"),
37
- B.releasedC("other:Source")
36
+ B.Consumed.claimed("other:Source", "second-producer"),
37
+ B.Consumed.released("other:Source")
38
38
  ])), B.requestFrom(B.transactional, "ref-10", "other:Source", "Default")), B.requested(B.transactional, "ref-10", B.announcedChannel, B.addressA)));
39
39
  let channel = B.unreachableChannel;
40
40
  if (channel !== undefined) {
41
41
  return G.test("a channel with no address on file is undeliverable, not suppressed", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([
42
- B.subscribedC(B.transactional, channel),
43
- B.unsubscribedC(B.transactional, B.announcedChannel)
42
+ B.Consumed.subscribed(B.transactional, channel),
43
+ B.Consumed.unsubscribed(B.transactional, B.announcedChannel)
44
44
  ])), B.request(B.transactional, "ref-5")), B.undeliverable(B.transactional, "ref-5")));
45
45
  }
46
46
  });
@@ -867,25 +867,27 @@ let conformanceBinding = (c: config): string => {
867
867
  ` module Spec = ${n.slice}`,
868
868
  ` module Behavior = ${n.slice}_Behavior`,
869
869
  ``,
870
- ` // A DCB slice's entity comes into existence with its first fact, so there is`,
871
- ` // no creation event to seed: an unannounced ${c.noun->String.toLowerCase} is one with no history.`,
872
- ` let created: array<Spec.consumedEvent> = []`,
873
- ``,
874
870
  ` let ${id} = "${c.noun->String.toLowerCase}-1"`,
875
871
  ``,
876
872
  ` // Annotated: this slice reads back exactly what it writes, so each of these`,
877
873
  ` // names a constructor of both unions and the later declaration would win.`,
878
- ` let announcedC = (${c.contactField}): Spec.consumedEvent =>`,
879
- ` ${n.announced}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
880
- ` let subscribedC = (category, channel): Spec.consumedEvent =>`,
881
- ` ${n.subscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
882
- ` let unsubscribedC = (category, channel): Spec.consumedEvent =>`,
883
- ` ${n.unsubscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
884
- ` // Written by ${n.claims} and read here across partitions — this slice`,
885
- ` // consumes them and produces neither.`,
886
- ` let claimedC = (source, by): Spec.consumedEvent =>`,
887
- ` ${n.claimed}({sourceId: source, by})`,
888
- ` let releasedC = (source): Spec.consumedEvent => ${n.released}({sourceId: source})`,
874
+ ` module Consumed = {`,
875
+ ` // A DCB slice's entity comes into existence with its first fact, so there`,
876
+ ` // is no creation event to seed: an unannounced ${c.noun->String.toLowerCase} is one with no`,
877
+ ` // history.`,
878
+ ` let created: array<Spec.consumedEvent> = []`,
879
+ ` let announced = (${c.contactField}): Spec.consumedEvent =>`,
880
+ ` ${n.announced}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
881
+ ` let subscribed = (category, channel): Spec.consumedEvent =>`,
882
+ ` ${n.subscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
883
+ ` let unsubscribed = (category, channel): Spec.consumedEvent =>`,
884
+ ` ${n.unsubscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
885
+ ` // Written by ${n.claims} and read here across partitions — this slice`,
886
+ ` // consumes them and produces neither.`,
887
+ ` let claimed = (source, by): Spec.consumedEvent =>`,
888
+ ` ${n.claimed}({sourceId: source, by})`,
889
+ ` let released = (source): Spec.consumedEvent => ${n.released}({sourceId: source})`,
890
+ ` }`,
889
891
  ``,
890
892
  ` let announce = ${c.contactField} =>`,
891
893
  ` Spec.${n.announceCmd}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
@@ -749,25 +749,27 @@ function conformanceBinding(c) {
749
749
  ` module Spec = ` + n.slice,
750
750
  ` module Behavior = ` + n.slice + `_Behavior`,
751
751
  ``,
752
- ` // A DCB slice's entity comes into existence with its first fact, so there is`,
753
- ` // no creation event to seed: an unannounced ` + c.noun.toLowerCase() + ` is one with no history.`,
754
- ` let created: array<Spec.consumedEvent> = []`,
755
- ``,
756
752
  ` let ` + id + ` = "` + c.noun.toLowerCase() + `-1"`,
757
753
  ``,
758
754
  ` // Annotated: this slice reads back exactly what it writes, so each of these`,
759
755
  ` // names a constructor of both unions and the later declaration would win.`,
760
- ` let announcedC = (` + c.contactField + `): Spec.consumedEvent =>`,
761
- ` ` + n.announced + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
762
- ` let subscribedC = (category, channel): Spec.consumedEvent =>`,
763
- ` ` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
764
- ` let unsubscribedC = (category, channel): Spec.consumedEvent =>`,
765
- ` ` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
766
- ` // Written by ` + n.claims + ` and read here across partitions this slice`,
767
- ` // consumes them and produces neither.`,
768
- ` let claimedC = (source, by): Spec.consumedEvent =>`,
769
- ` ` + n.claimed + `({sourceId: source, by})`,
770
- ` let releasedC = (source): Spec.consumedEvent => ` + n.released + `({sourceId: source})`,
756
+ ` module Consumed = {`,
757
+ ` // A DCB slice's entity comes into existence with its first fact, so there`,
758
+ ` // is no creation event to seed: an unannounced ` + c.noun.toLowerCase() + ` is one with no`,
759
+ ` // history.`,
760
+ ` let created: array<Spec.consumedEvent> = []`,
761
+ ` let announced = (` + c.contactField + `): Spec.consumedEvent =>`,
762
+ ` ` + n.announced + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
763
+ ` let subscribed = (category, channel): Spec.consumedEvent =>`,
764
+ ` ` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
765
+ ` let unsubscribed = (category, channel): Spec.consumedEvent =>`,
766
+ ` ` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
767
+ ` // Written by ` + n.claims + ` and read here across partitions — this slice`,
768
+ ` // consumes them and produces neither.`,
769
+ ` let claimed = (source, by): Spec.consumedEvent =>`,
770
+ ` ` + n.claimed + `({sourceId: source, by})`,
771
+ ` let released = (source): Spec.consumedEvent => ` + n.released + `({sourceId: source})`,
772
+ ` }`,
771
773
  ``,
772
774
  ` let announce = ` + c.contactField + ` =>`,
773
775
  ` Spec.` + n.announceCmd + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,