@reventlessdev/trait-notification 1.0.0-alpha.2 → 1.0.0-alpha.4
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 +3 -3
- package/src/Notification.res +27 -1
- package/src/Notification_Conformance.res +59 -0
- package/src/Notification_Conformance.res.mjs +8 -0
- package/src/Notification_Rule.res +248 -0
- package/src/Notification_Rule.res.mjs +348 -0
- package/src/Notification_Rules.res +75 -4
- package/src/Notification_Rules.res.mjs +76 -3
- package/src/Notification_Scaffold.res +458 -69
- package/src/Notification_Scaffold.res.mjs +437 -66
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
7
|
+
import * as Template$Reventless from "@reventlessdev/reventless-spec/src/semantic/Template.res.mjs";
|
|
8
|
+
|
|
9
|
+
let comparisonSchema = Sury.union([
|
|
10
|
+
Sury.literal("Eq"),
|
|
11
|
+
Sury.literal("Ne"),
|
|
12
|
+
Sury.literal("Lt"),
|
|
13
|
+
Sury.literal("Lte"),
|
|
14
|
+
Sury.literal("Gt"),
|
|
15
|
+
Sury.literal("Gte"),
|
|
16
|
+
Sury.literal("Contains")
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
let literalSchema = Sury.union([
|
|
20
|
+
Sury.$schema(s => ({
|
|
21
|
+
TAG: "Text",
|
|
22
|
+
_0: s.m(Sury.string)
|
|
23
|
+
})),
|
|
24
|
+
Sury.$schema(s => ({
|
|
25
|
+
TAG: "Number",
|
|
26
|
+
_0: s.m(Sury.float)
|
|
27
|
+
})),
|
|
28
|
+
Sury.$schema(s => ({
|
|
29
|
+
TAG: "Flag",
|
|
30
|
+
_0: s.m(Sury.bool)
|
|
31
|
+
}))
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
let predicateSchema = Sury.recursive("predicate", predicateSchema => Sury.union([
|
|
35
|
+
Sury.literal("Always"),
|
|
36
|
+
Sury.$schema(s => ({
|
|
37
|
+
TAG: "Compare",
|
|
38
|
+
path: s.m(Sury.string),
|
|
39
|
+
op: s.m(comparisonSchema),
|
|
40
|
+
value: s.m(literalSchema)
|
|
41
|
+
})),
|
|
42
|
+
Sury.$schema(s => ({
|
|
43
|
+
TAG: "All",
|
|
44
|
+
_0: s.m(Sury.array(predicateSchema))
|
|
45
|
+
})),
|
|
46
|
+
Sury.$schema(s => ({
|
|
47
|
+
TAG: "Any",
|
|
48
|
+
_0: s.m(Sury.array(predicateSchema))
|
|
49
|
+
})),
|
|
50
|
+
Sury.$schema(s => ({
|
|
51
|
+
TAG: "Not",
|
|
52
|
+
_0: s.m(predicateSchema)
|
|
53
|
+
}))
|
|
54
|
+
]));
|
|
55
|
+
|
|
56
|
+
let contentSchema = Sury.$schema(s => ({
|
|
57
|
+
locale: s.m(Sury.string),
|
|
58
|
+
subject: s.m(Sury.string),
|
|
59
|
+
body: s.m(Sury.string)
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
let sourceSchema = Sury.$schema(s => ({
|
|
63
|
+
log: s.m(Sury.string),
|
|
64
|
+
eventType: s.m(Sury.string)
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
let deliverySchema = Sury.union([
|
|
68
|
+
Sury.literal("Immediate"),
|
|
69
|
+
Sury.$schema(s => ({
|
|
70
|
+
TAG: "Digest",
|
|
71
|
+
windowSeconds: s.m(Sury.int)
|
|
72
|
+
}))
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
let schema = Sury.$schema(s => ({
|
|
76
|
+
id: s.m(Sury.string),
|
|
77
|
+
version: s.m(Sury.string),
|
|
78
|
+
source: s.m(sourceSchema),
|
|
79
|
+
filter: s.m(predicateSchema),
|
|
80
|
+
category: s.m(Sury.string),
|
|
81
|
+
delivery: s.m(deliverySchema),
|
|
82
|
+
recipientPath: s.m(Sury.string),
|
|
83
|
+
subjectType: s.m(Sury.string),
|
|
84
|
+
subjectPath: s.m(Sury.string),
|
|
85
|
+
content: s.m(Sury.array(contentSchema))
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
function sourceId(rule) {
|
|
89
|
+
return rule.source.log + `:` + rule.source.eventType;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function referenceFor(ruleId, subject) {
|
|
93
|
+
return ruleId + `:` + subject;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function reference(rule, subject) {
|
|
97
|
+
return referenceFor(rule.id, subject);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function byId(rules, id) {
|
|
101
|
+
return rules.find(rule => rule.id === id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isImmediate(rule) {
|
|
105
|
+
return rule.delivery === "Immediate";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function forEvent(rules, log, eventType) {
|
|
109
|
+
return rules.filter(rule => {
|
|
110
|
+
if (rule.source.log === log) {
|
|
111
|
+
return rule.source.eventType === eventType;
|
|
112
|
+
} else {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function compare(actual, op, expected) {
|
|
119
|
+
let ordered = (a, b) => {
|
|
120
|
+
switch (op) {
|
|
121
|
+
case "Eq" :
|
|
122
|
+
return Primitive_object.equal(a, b);
|
|
123
|
+
case "Ne" :
|
|
124
|
+
return Primitive_object.notequal(a, b);
|
|
125
|
+
case "Lt" :
|
|
126
|
+
return Primitive_object.lessthan(a, b);
|
|
127
|
+
case "Lte" :
|
|
128
|
+
return Primitive_object.lessequal(a, b);
|
|
129
|
+
case "Gt" :
|
|
130
|
+
return Primitive_object.greaterthan(a, b);
|
|
131
|
+
case "Gte" :
|
|
132
|
+
return Primitive_object.greaterequal(a, b);
|
|
133
|
+
case "Contains" :
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
if (actual === null) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(actual)) {
|
|
141
|
+
switch (expected.TAG) {
|
|
142
|
+
case "Text" :
|
|
143
|
+
let b = expected._0;
|
|
144
|
+
if (op === "Contains") {
|
|
145
|
+
return actual.some(item => {
|
|
146
|
+
if (typeof item === "string") {
|
|
147
|
+
return item === b;
|
|
148
|
+
} else {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
case "Number" :
|
|
156
|
+
case "Flag" :
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
switch (typeof actual) {
|
|
161
|
+
case "boolean" :
|
|
162
|
+
switch (expected.TAG) {
|
|
163
|
+
case "Text" :
|
|
164
|
+
case "Number" :
|
|
165
|
+
return false;
|
|
166
|
+
case "Flag" :
|
|
167
|
+
let b$1 = expected._0;
|
|
168
|
+
switch (op) {
|
|
169
|
+
case "Eq" :
|
|
170
|
+
return actual === b$1;
|
|
171
|
+
case "Ne" :
|
|
172
|
+
return actual !== b$1;
|
|
173
|
+
default:
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
case "string" :
|
|
178
|
+
switch (expected.TAG) {
|
|
179
|
+
case "Text" :
|
|
180
|
+
let b$2 = expected._0;
|
|
181
|
+
if (op === "Contains") {
|
|
182
|
+
return actual.includes(b$2);
|
|
183
|
+
} else {
|
|
184
|
+
return ordered(actual, b$2);
|
|
185
|
+
}
|
|
186
|
+
case "Number" :
|
|
187
|
+
case "Flag" :
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
case "number" :
|
|
191
|
+
switch (expected.TAG) {
|
|
192
|
+
case "Number" :
|
|
193
|
+
return ordered(actual, expected._0);
|
|
194
|
+
case "Text" :
|
|
195
|
+
case "Flag" :
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
case "object" :
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function matches(predicate, payload) {
|
|
205
|
+
if (typeof predicate !== "object") {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
switch (predicate.TAG) {
|
|
209
|
+
case "Compare" :
|
|
210
|
+
let actual = Template$Reventless.lookup(payload, predicate.path);
|
|
211
|
+
if (actual !== undefined) {
|
|
212
|
+
return compare(actual, predicate.op, predicate.value);
|
|
213
|
+
} else {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
case "All" :
|
|
217
|
+
return predicate._0.every(part => matches(part, payload));
|
|
218
|
+
case "Any" :
|
|
219
|
+
return predicate._0.some(part => matches(part, payload));
|
|
220
|
+
case "Not" :
|
|
221
|
+
return !matches(predicate._0, payload);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function stringAt(payload, path) {
|
|
226
|
+
let match = Template$Reventless.lookup(payload, path);
|
|
227
|
+
if (typeof match === "string") {
|
|
228
|
+
return match;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function recipientOf(rule, payload) {
|
|
233
|
+
return stringAt(payload, rule.recipientPath);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function subjectOf(rule, payload) {
|
|
237
|
+
return Stdlib_Option.getOr(stringAt(payload, rule.subjectPath), "");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function contentFor(rule, locale) {
|
|
241
|
+
if (locale === undefined) {
|
|
242
|
+
return rule.content[0];
|
|
243
|
+
}
|
|
244
|
+
let found = rule.content.find(entry => entry.locale === locale);
|
|
245
|
+
if (found !== undefined) {
|
|
246
|
+
return found;
|
|
247
|
+
} else {
|
|
248
|
+
return rule.content[0];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function render(source, payload, schema) {
|
|
253
|
+
return Stdlib_Result.getOr(Template$Reventless.renderSource(source, payload, schema), source);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function compose(rule, payload, schema, locale) {
|
|
257
|
+
let match = contentFor(rule, locale);
|
|
258
|
+
if (match !== undefined) {
|
|
259
|
+
return [
|
|
260
|
+
render(match.subject, payload, schema),
|
|
261
|
+
render(match.body, payload, schema)
|
|
262
|
+
];
|
|
263
|
+
} else {
|
|
264
|
+
return [
|
|
265
|
+
"",
|
|
266
|
+
""
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validate(rules, digestRoutedOpt, sample) {
|
|
272
|
+
let digestRouted = digestRoutedOpt !== undefined ? digestRoutedOpt : false;
|
|
273
|
+
return rules.flatMap(rule => {
|
|
274
|
+
let problems = [];
|
|
275
|
+
let note = message => {
|
|
276
|
+
problems.push(rule.id + `: ` + message);
|
|
277
|
+
};
|
|
278
|
+
if (rule.id === "") {
|
|
279
|
+
note("a rule needs an id — it is the namespace of every reference it writes");
|
|
280
|
+
}
|
|
281
|
+
if (rule.content.length === 0) {
|
|
282
|
+
note("no wording at all");
|
|
283
|
+
}
|
|
284
|
+
rule.content.forEach(param => {
|
|
285
|
+
let locale = param.locale;
|
|
286
|
+
[
|
|
287
|
+
[
|
|
288
|
+
"subject",
|
|
289
|
+
param.subject
|
|
290
|
+
],
|
|
291
|
+
[
|
|
292
|
+
"body",
|
|
293
|
+
param.body
|
|
294
|
+
]
|
|
295
|
+
].forEach(param => {
|
|
296
|
+
let why = Template$Reventless.parse(param[1]);
|
|
297
|
+
if (why.TAG === "Ok") {
|
|
298
|
+
return;
|
|
299
|
+
} else {
|
|
300
|
+
return note(locale + ` ` + param[0] + ` does not parse — ` + why._0);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
if (stringAt(sample, rule.recipientPath) === undefined) {
|
|
305
|
+
note(`recipientPath "` + rule.recipientPath + `" names nobody in the sample payload`);
|
|
306
|
+
}
|
|
307
|
+
if (stringAt(sample, rule.subjectPath) === undefined) {
|
|
308
|
+
note(`subjectPath "` + rule.subjectPath + `" resolves to nothing in the sample payload`);
|
|
309
|
+
}
|
|
310
|
+
let match = rule.delivery;
|
|
311
|
+
if (typeof match === "object") {
|
|
312
|
+
let windowSeconds = match.windowSeconds;
|
|
313
|
+
if (!digestRouted) {
|
|
314
|
+
note("delivered as a digest, and nothing in this deployment gathers one");
|
|
315
|
+
}
|
|
316
|
+
if (windowSeconds <= 0) {
|
|
317
|
+
note(`a digest window of ` + windowSeconds.toString() + `s gathers nothing`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return problems;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export {
|
|
325
|
+
comparisonSchema,
|
|
326
|
+
literalSchema,
|
|
327
|
+
predicateSchema,
|
|
328
|
+
contentSchema,
|
|
329
|
+
sourceSchema,
|
|
330
|
+
deliverySchema,
|
|
331
|
+
schema,
|
|
332
|
+
sourceId,
|
|
333
|
+
referenceFor,
|
|
334
|
+
reference,
|
|
335
|
+
byId,
|
|
336
|
+
isImmediate,
|
|
337
|
+
forEvent,
|
|
338
|
+
compare,
|
|
339
|
+
matches,
|
|
340
|
+
stringAt,
|
|
341
|
+
recipientOf,
|
|
342
|
+
subjectOf,
|
|
343
|
+
contentFor,
|
|
344
|
+
render,
|
|
345
|
+
compose,
|
|
346
|
+
validate,
|
|
347
|
+
}
|
|
348
|
+
/* comparisonSchema Not a pure module */
|
|
@@ -35,6 +35,21 @@ let channels = [Email, Sms, Push]
|
|
|
35
35
|
vocabulary — this module never enumerates them. */
|
|
36
36
|
type category = string
|
|
37
37
|
|
|
38
|
+
/** Which stream of occurrences a request came from, as `"<log>:<eventType>"`.
|
|
39
|
+
Opaque here: the competency never parses it, it only compares it against what
|
|
40
|
+
has been claimed. */
|
|
41
|
+
type source = string
|
|
42
|
+
|
|
43
|
+
/** Why a request exists. The compiled table that ships with a host, or a rule
|
|
44
|
+
somebody configured on top of it.
|
|
45
|
+
|
|
46
|
+
No payload on `Configured`: the trait needs exactly one thing from this — which
|
|
47
|
+
of the two yields when a source has been taken over — and *which* rule it was
|
|
48
|
+
is the host's fact to record on its own event. */
|
|
49
|
+
type origin =
|
|
50
|
+
| Default
|
|
51
|
+
| Configured
|
|
52
|
+
|
|
38
53
|
/** Refolded per decision, never stored — the host's slice state is. */
|
|
39
54
|
type t = {
|
|
40
55
|
/** One address per channel. An array rather than a single address because the
|
|
@@ -44,9 +59,16 @@ type t = {
|
|
|
44
59
|
contacts: array<(channel, string)>,
|
|
45
60
|
/** Only the cells a recipient has an opinion about; the rest is `~posture`. */
|
|
46
61
|
choices: array<(category, channel, bool)>,
|
|
62
|
+
/** Sources a second producer has taken over, and who took each one. Empty for
|
|
63
|
+
every host with one producer, which is what makes the deferral below inert
|
|
64
|
+
until something actually claims a source. */
|
|
65
|
+
claims: array<(source, string)>,
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
let empty = {contacts: [], choices: []}
|
|
68
|
+
let empty = {contacts: [], choices: [], claims: []}
|
|
69
|
+
|
|
70
|
+
let claimedBy = (t, source) =>
|
|
71
|
+
t.claims->Array.find(((s, _)) => s == source)->Option.map(((_, by)) => by)
|
|
50
72
|
|
|
51
73
|
/** What a host asks the competency to do. */
|
|
52
74
|
type op =
|
|
@@ -58,8 +80,15 @@ type op =
|
|
|
58
80
|
| Unsubscribe({category: category, channel: channel})
|
|
59
81
|
| /** Something worth telling them about happened. `reference` is the caller's
|
|
60
82
|
own key for it, echoed back on whichever outcome follows, so the relay
|
|
61
|
-
that asked can tell its work is finished. Opaque here on purpose.
|
|
62
|
-
|
|
83
|
+
that asked can tell its work is finished. Opaque here on purpose.
|
|
84
|
+
|
|
85
|
+
`source` says which stream it came from and `origin` says who is asking;
|
|
86
|
+
together they are the whole of the handover — see `decide`. */
|
|
87
|
+
Request({category: category, reference: string, source: source, origin: origin})
|
|
88
|
+
| /** A second producer takes over one source. Relayed like `Announce`: a
|
|
89
|
+
client that could claim a source would be silencing somebody else's mail. */
|
|
90
|
+
Claim({source: source, by: string})
|
|
91
|
+
| Release({source: source})
|
|
63
92
|
|
|
64
93
|
/** What the competency decided, for the host to name in its own events. */
|
|
65
94
|
type fact =
|
|
@@ -77,6 +106,19 @@ type fact =
|
|
|
77
106
|
reporting both the same way hides every delivery gap behind a legitimate
|
|
78
107
|
preference. */
|
|
79
108
|
Undeliverable({category: category, reference: string})
|
|
109
|
+
| /** Not acted on because another producer owns this source. A third way of
|
|
110
|
+
not sending, and its own fact for the same reason `Suppressed` and
|
|
111
|
+
`Undeliverable` are two rather than one: "they declined", "nobody was
|
|
112
|
+
reachable" and "somebody else has this" are three different things, and a
|
|
113
|
+
shared fact would hide two of them behind whichever name it took.
|
|
114
|
+
|
|
115
|
+
It is also load-bearing rather than tidy. The relay upstream resolves its
|
|
116
|
+
TODO row on an outcome; without a fourth one a deferred request retries
|
|
117
|
+
its whole budget and lands in `onExhausted`, which is a slow silent
|
|
118
|
+
failure with nothing in the log to explain it. */
|
|
119
|
+
Deferred({reference: string, source: source})
|
|
120
|
+
| Claimed({source: source, by: string})
|
|
121
|
+
| Released({source: source})
|
|
80
122
|
|
|
81
123
|
let addressFor = (t, channel) =>
|
|
82
124
|
t.contacts->Array.find(((c, _)) => c == channel)->Option.map(((_, address)) => address)
|
|
@@ -108,13 +150,22 @@ let evolve = (t, fact) =>
|
|
|
108
150
|
}
|
|
109
151
|
| Subscribed({category, channel}) => withChoice(t, category, channel, true)
|
|
110
152
|
| Unsubscribed({category, channel}) => withChoice(t, category, channel, false)
|
|
153
|
+
// Claims are per source and not per recipient, so they arrive from outside this
|
|
154
|
+
// recipient's own history — the host reads them across partitions, which is why
|
|
155
|
+
// the request carries the source it came from.
|
|
156
|
+
| Claimed({source, by}) => {
|
|
157
|
+
...t,
|
|
158
|
+
claims: Array.concat(t.claims->Array.filter(((s, _)) => s != source), [(source, by)]),
|
|
159
|
+
}
|
|
160
|
+
| Released({source}) => {...t, claims: t.claims->Array.filter(((s, _)) => s != source)}
|
|
111
161
|
// Neither the outcome of a request nor a delivery report changes who somebody
|
|
112
162
|
// is or what they want, so the fold ignores them. What a host must NOT do is
|
|
113
163
|
// read them back as consumed events to make this state bigger — the directory
|
|
114
164
|
// is snapshotted, and a set of every reference ever seen would grow forever.
|
|
115
165
|
| Requested(_)
|
|
116
166
|
| Suppressed(_)
|
|
117
|
-
| Undeliverable(_)
|
|
167
|
+
| Undeliverable(_)
|
|
168
|
+
| Deferred(_) => t
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
/** The whole competency, in one function.
|
|
@@ -150,6 +201,26 @@ let decide = (t, op, ~posture: (category, channel) => bool): result<
|
|
|
150
201
|
? Ok([Unsubscribed({category, channel})])
|
|
151
202
|
: Ok([])
|
|
152
203
|
|
|
204
|
+
| Claim({source: src, by}) =>
|
|
205
|
+
claimedBy(t, src) == Some(by) ? Ok([]) : Ok([Claimed({source: src, by})])
|
|
206
|
+
|
|
207
|
+
| Release({source: src}) =>
|
|
208
|
+
switch claimedBy(t, src) {
|
|
209
|
+
| None => Ok([])
|
|
210
|
+
| Some(_) => Ok([Released({source: src})])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The handover, and the whole of it. A `Configured` request is the second
|
|
214
|
+
// producer's own and always goes through; a `Default` one is the compiled
|
|
215
|
+
// table still firing for a source somebody else has taken over, and it yields.
|
|
216
|
+
//
|
|
217
|
+
// Precedence has to be per source rather than per deployment, which is why this
|
|
218
|
+
// is folded state and not a swapped-in implementation: a second producer that
|
|
219
|
+
// takes over some of the table and leaves the rest compiled cannot be expressed
|
|
220
|
+
// by choosing one of two implementations at build time.
|
|
221
|
+
| Request({source, origin: Default, reference}) if claimedBy(t, source) != None =>
|
|
222
|
+
Ok([Deferred({reference, source})])
|
|
223
|
+
|
|
153
224
|
| Request({category, reference}) =>
|
|
154
225
|
let wanted = channels->Array.filter(channel => enabled(t, ~posture, category, channel))
|
|
155
226
|
let addressed =
|
|
@@ -14,11 +14,18 @@ let empty_contacts = [];
|
|
|
14
14
|
|
|
15
15
|
let empty_choices = [];
|
|
16
16
|
|
|
17
|
+
let empty_claims = [];
|
|
18
|
+
|
|
17
19
|
let empty = {
|
|
18
20
|
contacts: empty_contacts,
|
|
19
|
-
choices: empty_choices
|
|
21
|
+
choices: empty_choices,
|
|
22
|
+
claims: empty_claims
|
|
20
23
|
};
|
|
21
24
|
|
|
25
|
+
function claimedBy(t, source) {
|
|
26
|
+
return Stdlib_Option.map(t.claims.find(param => param[0] === source), param => param[1]);
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
function addressFor(t, channel) {
|
|
23
30
|
return Stdlib_Option.map(t.contacts.find(param => param[0] === channel), param => param[1]);
|
|
24
31
|
}
|
|
@@ -45,7 +52,8 @@ function withChoice(t, category, channel, isEnabled) {
|
|
|
45
52
|
category,
|
|
46
53
|
channel,
|
|
47
54
|
isEnabled
|
|
48
|
-
]])
|
|
55
|
+
]]),
|
|
56
|
+
claims: t.claims
|
|
49
57
|
};
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -58,12 +66,30 @@ function evolve(t, fact) {
|
|
|
58
66
|
channel,
|
|
59
67
|
fact.address
|
|
60
68
|
]]),
|
|
61
|
-
choices: t.choices
|
|
69
|
+
choices: t.choices,
|
|
70
|
+
claims: t.claims
|
|
62
71
|
};
|
|
63
72
|
case "Subscribed" :
|
|
64
73
|
return withChoice(t, fact.category, fact.channel, true);
|
|
65
74
|
case "Unsubscribed" :
|
|
66
75
|
return withChoice(t, fact.category, fact.channel, false);
|
|
76
|
+
case "Claimed" :
|
|
77
|
+
let source = fact.source;
|
|
78
|
+
return {
|
|
79
|
+
contacts: t.contacts,
|
|
80
|
+
choices: t.choices,
|
|
81
|
+
claims: t.claims.filter(param => param[0] !== source).concat([[
|
|
82
|
+
source,
|
|
83
|
+
fact.by
|
|
84
|
+
]])
|
|
85
|
+
};
|
|
86
|
+
case "Released" :
|
|
87
|
+
let source$1 = fact.source;
|
|
88
|
+
return {
|
|
89
|
+
contacts: t.contacts,
|
|
90
|
+
choices: t.choices,
|
|
91
|
+
claims: t.claims.filter(param => param[0] !== source$1)
|
|
92
|
+
};
|
|
67
93
|
default:
|
|
68
94
|
return t;
|
|
69
95
|
}
|
|
@@ -138,8 +164,19 @@ function decide(t, op, posture) {
|
|
|
138
164
|
};
|
|
139
165
|
}
|
|
140
166
|
case "Request" :
|
|
167
|
+
let source = op.source;
|
|
141
168
|
let reference = op.reference;
|
|
142
169
|
let category$2 = op.category;
|
|
170
|
+
if (op.origin === "Default" && claimedBy(t, source) !== undefined) {
|
|
171
|
+
return {
|
|
172
|
+
TAG: "Ok",
|
|
173
|
+
_0: [{
|
|
174
|
+
TAG: "Deferred",
|
|
175
|
+
reference: reference,
|
|
176
|
+
source: source
|
|
177
|
+
}]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
143
180
|
let wanted = channels.filter(channel => enabled(t, posture, category$2, channel));
|
|
144
181
|
let addressed = Stdlib_Array.filterMap(wanted, channel => Stdlib_Option.map(addressFor(t, channel), address => ({
|
|
145
182
|
TAG: "Requested",
|
|
@@ -172,12 +209,48 @@ function decide(t, op, posture) {
|
|
|
172
209
|
}]
|
|
173
210
|
};
|
|
174
211
|
}
|
|
212
|
+
case "Claim" :
|
|
213
|
+
let by = op.by;
|
|
214
|
+
let src = op.source;
|
|
215
|
+
if (Primitive_object.equal(claimedBy(t, src), by)) {
|
|
216
|
+
return {
|
|
217
|
+
TAG: "Ok",
|
|
218
|
+
_0: []
|
|
219
|
+
};
|
|
220
|
+
} else {
|
|
221
|
+
return {
|
|
222
|
+
TAG: "Ok",
|
|
223
|
+
_0: [{
|
|
224
|
+
TAG: "Claimed",
|
|
225
|
+
source: src,
|
|
226
|
+
by: by
|
|
227
|
+
}]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "Release" :
|
|
231
|
+
let src$1 = op.source;
|
|
232
|
+
let match = claimedBy(t, src$1);
|
|
233
|
+
if (match !== undefined) {
|
|
234
|
+
return {
|
|
235
|
+
TAG: "Ok",
|
|
236
|
+
_0: [{
|
|
237
|
+
TAG: "Released",
|
|
238
|
+
source: src$1
|
|
239
|
+
}]
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
return {
|
|
243
|
+
TAG: "Ok",
|
|
244
|
+
_0: []
|
|
245
|
+
};
|
|
246
|
+
}
|
|
175
247
|
}
|
|
176
248
|
}
|
|
177
249
|
|
|
178
250
|
export {
|
|
179
251
|
channels,
|
|
180
252
|
empty,
|
|
253
|
+
claimedBy,
|
|
181
254
|
addressFor,
|
|
182
255
|
enabled,
|
|
183
256
|
withChoice,
|