@reventlessdev/trait-notification 1.0.0-alpha.4 → 1.0.0-alpha.40
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 +7 -7
- package/src/Notification.res +25 -19
- package/src/Notification_Conformance.res +27 -21
- package/src/Notification_Conformance.res.mjs +13 -13
- package/src/Notification_Rule.res +8 -12
- package/src/Notification_Rule.res.mjs +3 -3
- package/src/Notification_Rules.res +22 -24
- package/src/Notification_Scaffold.res +44 -45
- package/src/Notification_Scaffold.res.mjs +35 -26
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.40",
|
|
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": [
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"sury": "11.0.0
|
|
15
|
-
"sury-ppx": "11.0.0
|
|
16
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
17
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
14
|
+
"sury": "11.0.0",
|
|
15
|
+
"sury-ppx": "11.0.0",
|
|
16
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.238",
|
|
17
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.160"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"rescript": "12.3.
|
|
20
|
+
"rescript": "12.3.1"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"rescript": "12.3.
|
|
23
|
+
"rescript": "^12.3.1"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"registry": "https://registry.npmjs.org"
|
package/src/Notification.res
CHANGED
|
@@ -19,7 +19,6 @@ What an occurrence *is*. It is told a recipient, a kind, and a reference; it is
|
|
|
19
19
|
never told there was an order. That is why the two relays are the only files a
|
|
20
20
|
graft has to be given, and everything else can be handed over whole.
|
|
21
21
|
*/
|
|
22
|
-
|
|
23
22
|
/**
|
|
24
23
|
This trait's own account of itself — see `AddressGeocoding.declaration` for why the
|
|
25
24
|
version is read rather than written.
|
|
@@ -67,18 +66,30 @@ module type Binding = {
|
|
|
67
66
|
module Spec: ReventlessGwt.Behavior_GWT.BehaviorSpec
|
|
68
67
|
module Behavior: ReventlessGwt.Behavior_GWT.Behavior with module Spec = Spec
|
|
69
68
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
/** The same facts as the slice **consumes** them, for building history.
|
|
70
|
+
|
|
71
|
+
A slice declares `consumedEvent` and `event` separately and they differ —
|
|
72
|
+
the consumed form carries no recipient id, because the partition already
|
|
73
|
+
says whose it is. So a suite needs both spellings of the same fact: one to
|
|
74
|
+
lay down history, one to assert what was emitted.
|
|
75
|
+
|
|
76
|
+
Grouped rather than suffixed. The pairs used to read `announcedC` against
|
|
77
|
+
`announced`, which said "consumed" only to someone who already knew — and
|
|
78
|
+
in a codebase where every binding also carries commands, a bare `C` reads
|
|
79
|
+
just as naturally as one. */
|
|
80
|
+
module Consumed: {
|
|
81
|
+
/** History that brings the recipient into existence with nothing on file. */
|
|
82
|
+
let created: array<Spec.consumedEvent>
|
|
83
|
+
let announced: string => Spec.consumedEvent
|
|
84
|
+
let subscribed: (category, Notification_Rules.channel) => Spec.consumedEvent
|
|
85
|
+
let unsubscribed: (category, Notification_Rules.channel) => Spec.consumedEvent
|
|
86
|
+
/** A source taken over, as the slice reads it back. Claims are per source
|
|
87
|
+
and not per recipient, so a host produces these from wherever it keeps
|
|
88
|
+
them — what this contract requires is only that the decision can see
|
|
89
|
+
them. */
|
|
90
|
+
let claimed: (string, string) => Spec.consumedEvent
|
|
91
|
+
let released: string => Spec.consumedEvent
|
|
92
|
+
}
|
|
82
93
|
|
|
83
94
|
let announce: string => Spec.command
|
|
84
95
|
let subscribe: (category, Notification_Rules.channel) => Spec.command
|
|
@@ -108,12 +119,7 @@ module type Binding = {
|
|
|
108
119
|
/** `requested` as it comes out for a `Configured` request. A separate member
|
|
109
120
|
because *which* rule asked is the host's own shape — the trait's fact
|
|
110
121
|
carries no origin at all, so it cannot construct this one. */
|
|
111
|
-
let requestedConfigured: (
|
|
112
|
-
category,
|
|
113
|
-
string,
|
|
114
|
-
Notification_Rules.channel,
|
|
115
|
-
string,
|
|
116
|
-
) => Spec.event
|
|
122
|
+
let requestedConfigured: (category, string, Notification_Rules.channel, string) => Spec.event
|
|
117
123
|
|
|
118
124
|
/** The refusal for managing preferences for somebody nobody has announced. */
|
|
119
125
|
let recipientUnknown: Spec.error
|
|
@@ -6,7 +6,6 @@ What it asserts is the competency, not the host: the directory, the fallback to
|
|
|
6
6
|
the host's posture, and — the part worth being strict about — that the three ways
|
|
7
7
|
to send nothing stay three different facts.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
9
|
/** The suite's title, composed once and read twice: the suite registers it,
|
|
11
10
|
and `certify-trait` computes the same string to find the run's assertions in
|
|
12
11
|
a test report. Exported rather than inlined so neither side parses the
|
|
@@ -17,14 +16,14 @@ module Make = (B: Notification.Binding) => {
|
|
|
17
16
|
module G = ReventlessGwt.Behavior_GWT.Make(B.Spec, B.Behavior)
|
|
18
17
|
module R = Notification_Rules
|
|
19
18
|
|
|
20
|
-
let announced = Array.concat(B.created, [B.
|
|
19
|
+
let announced = Array.concat(B.Consumed.created, [B.Consumed.announced(B.addressA)])
|
|
21
20
|
|
|
22
21
|
let register = () =>
|
|
23
22
|
G.describe(suiteName(B.Spec.name), () => {
|
|
24
23
|
G.test("an announced contact is recorded", () =>
|
|
25
|
-
G.givenEvents(B.created)
|
|
26
|
-
|
|
27
|
-
)
|
|
24
|
+
G.givenEvents(B.Consumed.created)
|
|
25
|
+
->G.whenCmd(B.announce(B.addressA))
|
|
26
|
+
->G.thenEvent(B.announced(B.addressA))
|
|
28
27
|
)
|
|
29
28
|
|
|
30
29
|
// The relay re-announces on every contact event a host publishes, and its
|
|
@@ -36,15 +35,15 @@ module Make = (B: Notification.Binding) => {
|
|
|
36
35
|
)
|
|
37
36
|
|
|
38
37
|
G.test("a changed address is recorded", () =>
|
|
39
|
-
G.givenEvents(announced)
|
|
40
|
-
|
|
41
|
-
)
|
|
38
|
+
G.givenEvents(announced)
|
|
39
|
+
->G.whenCmd(B.announce(B.addressB))
|
|
40
|
+
->G.thenEvent(B.announced(B.addressB))
|
|
42
41
|
)
|
|
43
42
|
|
|
44
43
|
// A person is at the other end of this one, so they are told rather than
|
|
45
44
|
// having a fact recorded about them.
|
|
46
45
|
G.test("managing preferences for an unannounced recipient is refused", () =>
|
|
47
|
-
G.givenEvents(B.created)
|
|
46
|
+
G.givenEvents(B.Consumed.created)
|
|
48
47
|
->G.whenCmd(B.subscribe(B.optional, B.announcedChannel))
|
|
49
48
|
->G.thenError(B.recipientUnknown)
|
|
50
49
|
)
|
|
@@ -81,13 +80,11 @@ module Make = (B: Notification.Binding) => {
|
|
|
81
80
|
G.test("a transactional request goes out with no explicit subscription", () =>
|
|
82
81
|
G.givenEvents(announced)
|
|
83
82
|
->G.whenCmd(B.request(B.transactional, "ref-1"))
|
|
84
|
-
->G.thenEvent(
|
|
85
|
-
B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressA),
|
|
86
|
-
)
|
|
83
|
+
->G.thenEvent(B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressA))
|
|
87
84
|
)
|
|
88
85
|
|
|
89
86
|
G.test("the address on the request is the one currently on file", () =>
|
|
90
|
-
G.givenEvents(Array.concat(announced, [B.
|
|
87
|
+
G.givenEvents(Array.concat(announced, [B.Consumed.announced(B.addressB)]))
|
|
91
88
|
->G.whenCmd(B.request(B.transactional, "ref-1"))
|
|
92
89
|
->G.thenEvent(B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB))
|
|
93
90
|
)
|
|
@@ -100,7 +97,7 @@ module Make = (B: Notification.Binding) => {
|
|
|
100
97
|
|
|
101
98
|
G.test("a recipient who opted out is suppressed", () =>
|
|
102
99
|
G.givenEvents(
|
|
103
|
-
Array.concat(announced, [B.
|
|
100
|
+
Array.concat(announced, [B.Consumed.unsubscribed(B.transactional, B.announcedChannel)]),
|
|
104
101
|
)
|
|
105
102
|
->G.whenCmd(B.request(B.transactional, "ref-3"))
|
|
106
103
|
->G.thenEvent(B.suppressed(B.transactional, "ref-3"))
|
|
@@ -108,7 +105,7 @@ module Make = (B: Notification.Binding) => {
|
|
|
108
105
|
|
|
109
106
|
// The case the directory exists for.
|
|
110
107
|
G.test("a request for a recipient nobody announced is undeliverable", () =>
|
|
111
|
-
G.givenEvents(B.created)
|
|
108
|
+
G.givenEvents(B.Consumed.created)
|
|
112
109
|
->G.whenCmd(B.request(B.transactional, "ref-4"))
|
|
113
110
|
->G.thenEvent(B.undeliverable(B.transactional, "ref-4"))
|
|
114
111
|
)
|
|
@@ -129,7 +126,9 @@ module Make = (B: Notification.Binding) => {
|
|
|
129
126
|
)
|
|
130
127
|
|
|
131
128
|
G.test("a default request for a claimed source is deferred", () =>
|
|
132
|
-
G.givenEvents(
|
|
129
|
+
G.givenEvents(
|
|
130
|
+
Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]),
|
|
131
|
+
)
|
|
133
132
|
->G.whenCmd(
|
|
134
133
|
B.requestFrom(B.transactional, "ref-7", ~source="other:Source", ~origin=Default),
|
|
135
134
|
)
|
|
@@ -140,7 +139,9 @@ module Make = (B: Notification.Binding) => {
|
|
|
140
139
|
// keeps firing everywhere it was not taken over. Without this, a single
|
|
141
140
|
// claim would silence the whole competency.
|
|
142
141
|
G.test("a claim on one source leaves every other source alone", () =>
|
|
143
|
-
G.givenEvents(
|
|
142
|
+
G.givenEvents(
|
|
143
|
+
Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]),
|
|
144
|
+
)
|
|
144
145
|
->G.whenCmd(
|
|
145
146
|
B.requestFrom(B.transactional, "ref-8", ~source=B.defaultSource, ~origin=Default),
|
|
146
147
|
)
|
|
@@ -150,7 +151,9 @@ module Make = (B: Notification.Binding) => {
|
|
|
150
151
|
// The producer that owns the source is the one that must get through, or
|
|
151
152
|
// the handover would silence the entry rather than move it.
|
|
152
153
|
G.test("a configured request for a claimed source goes through", () =>
|
|
153
|
-
G.givenEvents(
|
|
154
|
+
G.givenEvents(
|
|
155
|
+
Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]),
|
|
156
|
+
)
|
|
154
157
|
->G.whenCmd(
|
|
155
158
|
B.requestFrom(B.transactional, "ref-9", ~source="other:Source", ~origin=Configured),
|
|
156
159
|
)
|
|
@@ -163,7 +166,10 @@ module Make = (B: Notification.Binding) => {
|
|
|
163
166
|
G.givenEvents(
|
|
164
167
|
Array.concat(
|
|
165
168
|
announced,
|
|
166
|
-
[
|
|
169
|
+
[
|
|
170
|
+
B.Consumed.claimed("other:Source", "second-producer"),
|
|
171
|
+
B.Consumed.released("other:Source"),
|
|
172
|
+
],
|
|
167
173
|
),
|
|
168
174
|
)
|
|
169
175
|
->G.whenCmd(
|
|
@@ -183,8 +189,8 @@ module Make = (B: Notification.Binding) => {
|
|
|
183
189
|
Array.concat(
|
|
184
190
|
announced,
|
|
185
191
|
[
|
|
186
|
-
B.
|
|
187
|
-
B.
|
|
192
|
+
B.Consumed.subscribed(B.transactional, channel),
|
|
193
|
+
B.Consumed.unsubscribed(B.transactional, B.announcedChannel),
|
|
188
194
|
],
|
|
189
195
|
),
|
|
190
196
|
)
|
|
@@ -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
|
});
|
|
@@ -9,7 +9,6 @@ surface. It is also why the wording is `Reventless.Template` source: readable
|
|
|
9
9
|
text rather than ReScript interpolation, rendered against the payload's schema so
|
|
10
10
|
a semantic formats itself and a `@sensitive` field is withheld.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
12
|
/** How a field is compared. */
|
|
14
13
|
@schema
|
|
15
14
|
type comparison =
|
|
@@ -165,8 +164,7 @@ let recipientOf = (rule: t, ~payload: JSON.t) => stringAt(payload, rule.recipien
|
|
|
165
164
|
|
|
166
165
|
/** What it is about. Empty is legal — a notification about nothing in particular
|
|
167
166
|
is a real case, and a fabricated subject is worse than an absent one. */
|
|
168
|
-
let subjectOf = (rule: t, ~payload: JSON.t) =>
|
|
169
|
-
stringAt(payload, rule.subjectPath)->Option.getOr("")
|
|
167
|
+
let subjectOf = (rule: t, ~payload: JSON.t) => stringAt(payload, rule.subjectPath)->Option.getOr("")
|
|
170
168
|
|
|
171
169
|
/** The wording to use, the asked-for locale if the rule has it and the first
|
|
172
170
|
otherwise. Which locale to ask for is the caller's — nothing here knows who
|
|
@@ -191,10 +189,7 @@ let render = (source: string, ~payload: JSON.t, ~schema: S.t<'a>) =>
|
|
|
191
189
|
let compose = (rule: t, ~payload: JSON.t, ~schema: S.t<'a>, ~locale: option<string>=?) =>
|
|
192
190
|
switch contentFor(rule, ~locale?) {
|
|
193
191
|
| None => ("", "")
|
|
194
|
-
| Some({subject, body}) => (
|
|
195
|
-
render(subject, ~payload, ~schema),
|
|
196
|
-
render(body, ~payload, ~schema),
|
|
197
|
-
)
|
|
192
|
+
| Some({subject, body}) => (render(subject, ~payload, ~schema), render(body, ~payload, ~schema))
|
|
198
193
|
}
|
|
199
194
|
|
|
200
195
|
/**
|
|
@@ -221,11 +216,12 @@ let validate = (rules: array<t>, ~digestRouted: bool=false, ~sample: JSON.t): ar
|
|
|
221
216
|
note("no wording at all")
|
|
222
217
|
}
|
|
223
218
|
rule.content->Array.forEach(({locale, subject, body}) =>
|
|
224
|
-
[("subject", subject), ("body", body)]->Array.forEach(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
[("subject", subject), ("body", body)]->Array.forEach(
|
|
220
|
+
((which, template)) =>
|
|
221
|
+
switch Reventless.Template.parse(template) {
|
|
222
|
+
| Ok(_) => ()
|
|
223
|
+
| Error(why) => note(`${locale} ${which} does not parse — ${why}`)
|
|
224
|
+
},
|
|
229
225
|
)
|
|
230
226
|
)
|
|
231
227
|
if recipientOf(rule, ~payload=sample) == None {
|
|
@@ -23,11 +23,11 @@ let literalSchema = Sury.union([
|
|
|
23
23
|
})),
|
|
24
24
|
Sury.$schema(s => ({
|
|
25
25
|
TAG: "Number",
|
|
26
|
-
_0: s.m(Sury.
|
|
26
|
+
_0: s.m(Sury.number)
|
|
27
27
|
})),
|
|
28
28
|
Sury.$schema(s => ({
|
|
29
29
|
TAG: "Flag",
|
|
30
|
-
_0: s.m(Sury.
|
|
30
|
+
_0: s.m(Sury.boolean)
|
|
31
31
|
}))
|
|
32
32
|
]);
|
|
33
33
|
|
|
@@ -68,7 +68,7 @@ let deliverySchema = Sury.union([
|
|
|
68
68
|
Sury.literal("Immediate"),
|
|
69
69
|
Sury.$schema(s => ({
|
|
70
70
|
TAG: "Digest",
|
|
71
|
-
windowSeconds: s.m(Sury.
|
|
71
|
+
windowSeconds: s.m(Sury.int32)
|
|
72
72
|
}))
|
|
73
73
|
]);
|
|
74
74
|
|
|
@@ -17,7 +17,6 @@ is the opposite. So the fallback *rule* lives here — an absent choice defers t
|
|
|
17
17
|
the posture — and the *table* is passed in as `~posture`. A trait that hard-coded
|
|
18
18
|
either answer would be wrong for half its hosts.
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
20
|
/** How a recipient is reached. Mirrors `Reventless.Messaging.channel`, which is
|
|
22
21
|
the platform capability's vocabulary; this one is the domain's, so it carries
|
|
23
22
|
a host's schema and travels on the wire. `Notification_Send` maps the two. */
|
|
@@ -72,22 +71,22 @@ let claimedBy = (t, source) =>
|
|
|
72
71
|
|
|
73
72
|
/** What a host asks the competency to do. */
|
|
74
73
|
type op =
|
|
75
|
-
|
|
74
|
+
/** Relayed from whatever the host publishes when it learns where somebody is.
|
|
76
75
|
Not a client's command: a caller who could announce another person's
|
|
77
76
|
address would be redirecting their mail. */
|
|
78
|
-
Announce({channel: channel, address: string})
|
|
77
|
+
| Announce({channel: channel, address: string})
|
|
79
78
|
| Subscribe({category: category, channel: channel})
|
|
80
79
|
| Unsubscribe({category: category, channel: channel})
|
|
81
|
-
|
|
80
|
+
/** Something worth telling them about happened. `reference` is the caller's
|
|
82
81
|
own key for it, echoed back on whichever outcome follows, so the relay
|
|
83
82
|
that asked can tell its work is finished. Opaque here on purpose.
|
|
84
83
|
|
|
85
84
|
`source` says which stream it came from and `origin` says who is asking;
|
|
86
85
|
together they are the whole of the handover — see `decide`. */
|
|
87
|
-
Request({category: category, reference: string, source: source, origin: origin})
|
|
88
|
-
|
|
86
|
+
| Request({category: category, reference: string, source: source, origin: origin})
|
|
87
|
+
/** A second producer takes over one source. Relayed like `Announce`: a
|
|
89
88
|
client that could claim a source would be silencing somebody else's mail. */
|
|
90
|
-
Claim({source: source, by: string})
|
|
89
|
+
| Claim({source: source, by: string})
|
|
91
90
|
| Release({source: source})
|
|
92
91
|
|
|
93
92
|
/** What the competency decided, for the host to name in its own events. */
|
|
@@ -95,18 +94,18 @@ type fact =
|
|
|
95
94
|
| Announced({channel: channel, address: string})
|
|
96
95
|
| Subscribed({category: category, channel: channel})
|
|
97
96
|
| Unsubscribed({category: category, channel: channel})
|
|
98
|
-
|
|
97
|
+
/** The addressed message. `address` is the snapshot delivery uses, which is
|
|
99
98
|
why it is on the fact and not looked up again later. */
|
|
100
|
-
Requested({category: category, reference: string, channel: channel, address: string})
|
|
101
|
-
|
|
102
|
-
Suppressed({category: category, reference: string})
|
|
103
|
-
|
|
99
|
+
| Requested({category: category, reference: string, channel: channel, address: string})
|
|
100
|
+
/** They are reachable and said no. */
|
|
101
|
+
| Suppressed({category: category, reference: string})
|
|
102
|
+
/** They want it and nobody can be reached — no address on file for any
|
|
104
103
|
channel they left enabled. A different fact from `Suppressed` on purpose:
|
|
105
104
|
one is the system working and the other is the system falling short, and
|
|
106
105
|
reporting both the same way hides every delivery gap behind a legitimate
|
|
107
106
|
preference. */
|
|
108
|
-
Undeliverable({category: category, reference: string})
|
|
109
|
-
|
|
107
|
+
| Undeliverable({category: category, reference: string})
|
|
108
|
+
/** Not acted on because another producer owns this source. A third way of
|
|
110
109
|
not sending, and its own fact for the same reason `Suppressed` and
|
|
111
110
|
`Undeliverable` are two rather than one: "they declined", "nobody was
|
|
112
111
|
reachable" and "somebody else has this" are three different things, and a
|
|
@@ -116,7 +115,7 @@ type fact =
|
|
|
116
115
|
TODO row on an outcome; without a fourth one a deferred request retries
|
|
117
116
|
its whole budget and lands in `onExhausted`, which is a slow silent
|
|
118
117
|
failure with nothing in the log to explain it. */
|
|
119
|
-
Deferred({reference: string, source: source})
|
|
118
|
+
| Deferred({reference: string, source: source})
|
|
120
119
|
| Claimed({source: source, by: string})
|
|
121
120
|
| Released({source: source})
|
|
122
121
|
|
|
@@ -223,15 +222,14 @@ let decide = (t, op, ~posture: (category, channel) => bool): result<
|
|
|
223
222
|
|
|
224
223
|
| Request({category, reference}) =>
|
|
225
224
|
let wanted = channels->Array.filter(channel => enabled(t, ~posture, category, channel))
|
|
226
|
-
let addressed =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
225
|
+
let addressed = wanted->Array.filterMap(channel =>
|
|
226
|
+
addressFor(t, channel)->Option.map(address => Requested({
|
|
227
|
+
category,
|
|
228
|
+
reference,
|
|
229
|
+
channel,
|
|
230
|
+
address,
|
|
231
|
+
}))
|
|
232
|
+
)
|
|
235
233
|
switch addressed {
|
|
236
234
|
| [] =>
|
|
237
235
|
// Wanted-but-unreachable and not-wanted are the two ways to send nothing,
|
|
@@ -15,7 +15,6 @@ is the one thing a trait cannot be told in names.
|
|
|
15
15
|
says is the host's sentence, so the emitted rule table carries a `TODO(graft)`
|
|
16
16
|
marker in place of one — it compiles, runs, and is obvious in a diff.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
18
|
/**
|
|
20
19
|
The names a graft needs, and nothing else.
|
|
21
20
|
|
|
@@ -137,8 +136,8 @@ let namesOf = (c: config): names => {
|
|
|
137
136
|
delivered: "NotificationDelivered",
|
|
138
137
|
failed: "NotificationFailed",
|
|
139
138
|
unknownError: n ++ "Unknown",
|
|
140
|
-
recipientId:
|
|
141
|
-
|
|
139
|
+
recipientId: n->String.slice(~start=0, ~end=1)->String.toLowerCase ++
|
|
140
|
+
n->String.slice(~start=1, ~end=n->String.length) ++ "Id",
|
|
142
141
|
}
|
|
143
142
|
}
|
|
144
143
|
|
|
@@ -154,13 +153,14 @@ let addressB = (c: config) => c.addressB->Option.getOr("new@example.com")
|
|
|
154
153
|
graft, so a host whose component is registered under another name edits one
|
|
155
154
|
line rather than discovering the convention. */
|
|
156
155
|
let subjectTypeOf = (c: config) => {
|
|
157
|
-
let stem =
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
let stem =
|
|
157
|
+
c.occurrenceId->String.endsWith("Id")
|
|
158
|
+
? c.occurrenceId->String.slice(~start=0, ~end=c.occurrenceId->String.length - 2)
|
|
159
|
+
: c.occurrenceId
|
|
160
160
|
switch stem {
|
|
161
161
|
| "" => "Subject"
|
|
162
162
|
| s =>
|
|
163
|
-
|
|
163
|
+
s->String.slice(~start=0, ~end=1)->String.toUpperCase ++
|
|
164
164
|
s->String.slice(~start=1, ~end=s->String.length)
|
|
165
165
|
}
|
|
166
166
|
}
|
|
@@ -373,19 +373,9 @@ let sliceBehavior = (c: config): string => {
|
|
|
373
373
|
` switch (category, channel) {`,
|
|
374
374
|
],
|
|
375
375
|
postureArms(c),
|
|
376
|
-
[
|
|
377
|
-
` }`,
|
|
378
|
-
``,
|
|
379
|
-
`let categoryKey = (category: category) =>`,
|
|
380
|
-
` switch category {`,
|
|
381
|
-
],
|
|
376
|
+
[` }`, ``, `let categoryKey = (category: category) =>`, ` switch category {`],
|
|
382
377
|
c.categories->Array.map(cat => ` | ${cat} => "${cat}"`),
|
|
383
|
-
[
|
|
384
|
-
` }`,
|
|
385
|
-
``,
|
|
386
|
-
`let categoryOf = (key: string) =>`,
|
|
387
|
-
` switch key {`,
|
|
388
|
-
],
|
|
378
|
+
[` }`, ``, `let categoryOf = (key: string) =>`, ` switch key {`],
|
|
389
379
|
c.categories
|
|
390
380
|
->Array.filter(cat => cat != first)
|
|
391
381
|
->Array.map(cat => ` | "${cat}" => ${cat}`),
|
|
@@ -773,13 +763,20 @@ let sendTranslation = (c: config): string => {
|
|
|
773
763
|
` item.address`,
|
|
774
764
|
` ->Reventless.Phone.fromString`,
|
|
775
765
|
` ->Result.map(phone => Reventless.Messaging.ToSms(phone))`,
|
|
776
|
-
`
|
|
766
|
+
` // The directory stores a channel beside one flat address, and no push address is`,
|
|
767
|
+
` // one string: two services issue a token, Web Push issues an endpoint with its`,
|
|
768
|
+
` // encryption keys, and which service issued this one is not recorded either. So`,
|
|
769
|
+
` // the pair cannot be re-fused, and refusing says so once rather than guessing a`,
|
|
770
|
+
` // service and failing at whichever provider. A push transport arrives with the`,
|
|
771
|
+
` // stored shape it needs, in the commit that justifies the columns.`,
|
|
772
|
+
` | Push =>`,
|
|
773
|
+
` Error("a push address names its issuing service, and the directory stores one flat address")`,
|
|
777
774
|
` }`,
|
|
778
775
|
``,
|
|
779
776
|
`let translate = async (_id, item: outboundItem, ~capabilities: Reventless.Capabilities.t) =>`,
|
|
780
777
|
` switch recipientFor(item) {`,
|
|
781
|
-
` // An address the directory holds that its own channel
|
|
782
|
-
` //
|
|
778
|
+
` // An address the directory holds that its own channel cannot use. Not retryable`,
|
|
779
|
+
` // and not the provider's fault — the row that holds it needs fixing.`,
|
|
783
780
|
` | Error(why) =>`,
|
|
784
781
|
` Ok(`,
|
|
785
782
|
` Some((`,
|
|
@@ -867,25 +864,27 @@ let conformanceBinding = (c: config): string => {
|
|
|
867
864
|
` module Spec = ${n.slice}`,
|
|
868
865
|
` module Behavior = ${n.slice}_Behavior`,
|
|
869
866
|
``,
|
|
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
867
|
` let ${id} = "${c.noun->String.toLowerCase}-1"`,
|
|
875
868
|
``,
|
|
876
869
|
` // Annotated: this slice reads back exactly what it writes, so each of these`,
|
|
877
870
|
` // names a constructor of both unions and the later declaration would win.`,
|
|
878
|
-
`
|
|
879
|
-
`
|
|
880
|
-
`
|
|
881
|
-
`
|
|
882
|
-
`
|
|
883
|
-
` ${
|
|
884
|
-
`
|
|
885
|
-
`
|
|
886
|
-
`
|
|
887
|
-
`
|
|
888
|
-
`
|
|
871
|
+
` module Consumed = {`,
|
|
872
|
+
` // A DCB slice's entity comes into existence with its first fact, so there`,
|
|
873
|
+
` // is no creation event to seed: an unannounced ${c.noun->String.toLowerCase} is one with no`,
|
|
874
|
+
` // history.`,
|
|
875
|
+
` let created: array<Spec.consumedEvent> = []`,
|
|
876
|
+
` let announced = (${c.contactField}): Spec.consumedEvent =>`,
|
|
877
|
+
` ${n.announced}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
|
|
878
|
+
` let subscribed = (category, channel): Spec.consumedEvent =>`,
|
|
879
|
+
` ${n.subscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
|
|
880
|
+
` let unsubscribed = (category, channel): Spec.consumedEvent =>`,
|
|
881
|
+
` ${n.unsubscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
|
|
882
|
+
` // Written by ${n.claims} and read here across partitions — this slice`,
|
|
883
|
+
` // consumes them and produces neither.`,
|
|
884
|
+
` let claimed = (source, by): Spec.consumedEvent =>`,
|
|
885
|
+
` ${n.claimed}({sourceId: source, by})`,
|
|
886
|
+
` let released = (source): Spec.consumedEvent => ${n.released}({sourceId: source})`,
|
|
887
|
+
` }`,
|
|
889
888
|
``,
|
|
890
889
|
` let announce = ${c.contactField} =>`,
|
|
891
890
|
` Spec.${n.announceCmd}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
|
|
@@ -1003,7 +1002,7 @@ let contactRelayPatch = (c: config): patch => {
|
|
|
1003
1002
|
let n = namesOf(c)
|
|
1004
1003
|
let id = n.recipientId
|
|
1005
1004
|
{
|
|
1006
|
-
into: `${c.chapter}/
|
|
1005
|
+
into: `${c.chapter}/OutboundTranslation/${n.relay}.res (new, plus its _Translation)`,
|
|
1007
1006
|
at: `a new file — the trait cannot write what this host's events mean`,
|
|
1008
1007
|
contents: lines(
|
|
1009
1008
|
Array.flatMap(
|
|
@@ -1083,7 +1082,7 @@ let intakeRelayPatch = (c: config): patch => {
|
|
|
1083
1082
|
let n = namesOf(c)
|
|
1084
1083
|
let id = n.recipientId
|
|
1085
1084
|
{
|
|
1086
|
-
into: `${c.chapter}/
|
|
1085
|
+
into: `${c.chapter}/Automation/${n.intake}.res (new, plus its _Automation)`,
|
|
1087
1086
|
at: `a new file — the wording is this host's sentence, not the trait's`,
|
|
1088
1087
|
contents: lines([
|
|
1089
1088
|
`// Turns one of this host's occurrences into a request to notify somebody.`,
|
|
@@ -1264,16 +1263,16 @@ let emit = (~config: config, ~into: string, ~tests: string): output => {
|
|
|
1264
1263
|
let n = namesOf(config)
|
|
1265
1264
|
{
|
|
1266
1265
|
files: [
|
|
1267
|
-
{path: `${into}/
|
|
1268
|
-
{path: `${into}/
|
|
1269
|
-
{path: `${into}/
|
|
1266
|
+
{path: `${into}/StateChange/${n.slice}.res`, contents: sliceSpec(config)},
|
|
1267
|
+
{path: `${into}/StateChange/${n.slice}_Behavior.res`, contents: sliceBehavior(config)},
|
|
1268
|
+
{path: `${into}/StateChange/${n.claims}.res`, contents: claimsSpec(config)},
|
|
1270
1269
|
{
|
|
1271
|
-
path: `${into}/
|
|
1270
|
+
path: `${into}/StateChange/${n.claims}_Behavior.res`,
|
|
1272
1271
|
contents: claimsBehavior(config),
|
|
1273
1272
|
},
|
|
1274
|
-
{path: `${into}/
|
|
1273
|
+
{path: `${into}/OutboundTranslation/${n.send}.res`, contents: sendSpec(config)},
|
|
1275
1274
|
{
|
|
1276
|
-
path: `${into}/
|
|
1275
|
+
path: `${into}/OutboundTranslation/${n.send}_Translation.res`,
|
|
1277
1276
|
contents: sendTranslation(config),
|
|
1278
1277
|
},
|
|
1279
1278
|
{path: `${tests}/NotificationConformance_GWT.res`, contents: conformanceBinding(config)},
|
|
@@ -660,13 +660,20 @@ function sendTranslation(c) {
|
|
|
660
660
|
` item.address`,
|
|
661
661
|
` ->Reventless.Phone.fromString`,
|
|
662
662
|
` ->Result.map(phone => Reventless.Messaging.ToSms(phone))`,
|
|
663
|
-
`
|
|
663
|
+
` // The directory stores a channel beside one flat address, and no push address is`,
|
|
664
|
+
` // one string: two services issue a token, Web Push issues an endpoint with its`,
|
|
665
|
+
` // encryption keys, and which service issued this one is not recorded either. So`,
|
|
666
|
+
` // the pair cannot be re-fused, and refusing says so once rather than guessing a`,
|
|
667
|
+
` // service and failing at whichever provider. A push transport arrives with the`,
|
|
668
|
+
` // stored shape it needs, in the commit that justifies the columns.`,
|
|
669
|
+
` | Push =>`,
|
|
670
|
+
` Error("a push address names its issuing service, and the directory stores one flat address")`,
|
|
664
671
|
` }`,
|
|
665
672
|
``,
|
|
666
673
|
`let translate = async (_id, item: outboundItem, ~capabilities: Reventless.Capabilities.t) =>`,
|
|
667
674
|
` switch recipientFor(item) {`,
|
|
668
|
-
` // An address the directory holds that its own channel
|
|
669
|
-
` //
|
|
675
|
+
` // An address the directory holds that its own channel cannot use. Not retryable`,
|
|
676
|
+
` // and not the provider's fault — the row that holds it needs fixing.`,
|
|
670
677
|
` | Error(why) =>`,
|
|
671
678
|
` Ok(`,
|
|
672
679
|
` Some((`,
|
|
@@ -749,25 +756,27 @@ function conformanceBinding(c) {
|
|
|
749
756
|
` module Spec = ` + n.slice,
|
|
750
757
|
` module Behavior = ` + n.slice + `_Behavior`,
|
|
751
758
|
``,
|
|
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
759
|
` let ` + id + ` = "` + c.noun.toLowerCase() + `-1"`,
|
|
757
760
|
``,
|
|
758
761
|
` // Annotated: this slice reads back exactly what it writes, so each of these`,
|
|
759
762
|
` // names a constructor of both unions and the later declaration would win.`,
|
|
760
|
-
`
|
|
761
|
-
`
|
|
762
|
-
`
|
|
763
|
-
`
|
|
764
|
-
`
|
|
765
|
-
`
|
|
766
|
-
`
|
|
767
|
-
`
|
|
768
|
-
`
|
|
769
|
-
`
|
|
770
|
-
`
|
|
763
|
+
` module Consumed = {`,
|
|
764
|
+
` // A DCB slice's entity comes into existence with its first fact, so there`,
|
|
765
|
+
` // is no creation event to seed: an unannounced ` + c.noun.toLowerCase() + ` is one with no`,
|
|
766
|
+
` // history.`,
|
|
767
|
+
` let created: array<Spec.consumedEvent> = []`,
|
|
768
|
+
` let announced = (` + c.contactField + `): Spec.consumedEvent =>`,
|
|
769
|
+
` ` + n.announced + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
|
|
770
|
+
` let subscribed = (category, channel): Spec.consumedEvent =>`,
|
|
771
|
+
` ` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
|
|
772
|
+
` let unsubscribed = (category, channel): Spec.consumedEvent =>`,
|
|
773
|
+
` ` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
|
|
774
|
+
` // Written by ` + n.claims + ` and read here across partitions — this slice`,
|
|
775
|
+
` // consumes them and produces neither.`,
|
|
776
|
+
` let claimed = (source, by): Spec.consumedEvent =>`,
|
|
777
|
+
` ` + n.claimed + `({sourceId: source, by})`,
|
|
778
|
+
` let released = (source): Spec.consumedEvent => ` + n.released + `({sourceId: source})`,
|
|
779
|
+
` }`,
|
|
771
780
|
``,
|
|
772
781
|
` let announce = ` + c.contactField + ` =>`,
|
|
773
782
|
` Spec.` + n.announceCmd + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
|
|
@@ -877,7 +886,7 @@ function contactRelayPatch(c) {
|
|
|
877
886
|
let n = namesOf(c);
|
|
878
887
|
let id = n.recipientId;
|
|
879
888
|
return {
|
|
880
|
-
into: c.chapter + `/
|
|
889
|
+
into: c.chapter + `/OutboundTranslation/` + n.relay + `.res (new, plus its _Translation)`,
|
|
881
890
|
at: `a new file — the trait cannot write what this host's events mean`,
|
|
882
891
|
contents: [
|
|
883
892
|
[
|
|
@@ -950,7 +959,7 @@ function intakeRelayPatch(c) {
|
|
|
950
959
|
let n = namesOf(c);
|
|
951
960
|
let id = n.recipientId;
|
|
952
961
|
return {
|
|
953
|
-
into: c.chapter + `/
|
|
962
|
+
into: c.chapter + `/Automation/` + n.intake + `.res (new, plus its _Automation)`,
|
|
954
963
|
at: `a new file — the wording is this host's sentence, not the trait's`,
|
|
955
964
|
contents: [
|
|
956
965
|
`// Turns one of this host's occurrences into a request to notify somebody.`,
|
|
@@ -1126,27 +1135,27 @@ function emit(config, into, tests) {
|
|
|
1126
1135
|
return {
|
|
1127
1136
|
files: [
|
|
1128
1137
|
{
|
|
1129
|
-
path: into + `/
|
|
1138
|
+
path: into + `/StateChange/` + n.slice + `.res`,
|
|
1130
1139
|
contents: sliceSpec(config)
|
|
1131
1140
|
},
|
|
1132
1141
|
{
|
|
1133
|
-
path: into + `/
|
|
1142
|
+
path: into + `/StateChange/` + n.slice + `_Behavior.res`,
|
|
1134
1143
|
contents: sliceBehavior(config)
|
|
1135
1144
|
},
|
|
1136
1145
|
{
|
|
1137
|
-
path: into + `/
|
|
1146
|
+
path: into + `/StateChange/` + n.claims + `.res`,
|
|
1138
1147
|
contents: claimsSpec(config)
|
|
1139
1148
|
},
|
|
1140
1149
|
{
|
|
1141
|
-
path: into + `/
|
|
1150
|
+
path: into + `/StateChange/` + n.claims + `_Behavior.res`,
|
|
1142
1151
|
contents: claimsBehavior(config)
|
|
1143
1152
|
},
|
|
1144
1153
|
{
|
|
1145
|
-
path: into + `/
|
|
1154
|
+
path: into + `/OutboundTranslation/` + n.send + `.res`,
|
|
1146
1155
|
contents: sendSpec(config)
|
|
1147
1156
|
},
|
|
1148
1157
|
{
|
|
1149
|
-
path: into + `/
|
|
1158
|
+
path: into + `/OutboundTranslation/` + n.send + `_Translation.res`,
|
|
1150
1159
|
contents: sendTranslation(config)
|
|
1151
1160
|
},
|
|
1152
1161
|
{
|