@reventlessdev/trait-notification 1.0.0-alpha.4 → 1.0.0-alpha.6
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.
|
|
3
|
+
"version": "1.0.0-alpha.6",
|
|
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.
|
|
17
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
16
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.204",
|
|
17
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.130"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"rescript": "12.3.0"
|
package/src/Notification.res
CHANGED
|
@@ -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
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
187
|
-
B.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
-
G.test("a claim on one source leaves every other source alone", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.
|
|
34
|
-
G.test("a configured request for a claimed source goes through", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.
|
|
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.
|
|
37
|
-
B.
|
|
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.
|
|
43
|
-
B.
|
|
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
|
-
`
|
|
879
|
-
`
|
|
880
|
-
`
|
|
881
|
-
`
|
|
882
|
-
`
|
|
883
|
-
` ${
|
|
884
|
-
`
|
|
885
|
-
`
|
|
886
|
-
`
|
|
887
|
-
`
|
|
888
|
-
`
|
|
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
|
-
`
|
|
761
|
-
`
|
|
762
|
-
`
|
|
763
|
-
`
|
|
764
|
-
`
|
|
765
|
-
`
|
|
766
|
-
`
|
|
767
|
-
`
|
|
768
|
-
`
|
|
769
|
-
`
|
|
770
|
-
`
|
|
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 + `})`,
|