@reventlessdev/reventless-core 3.0.0-alpha.245 → 3.0.0-alpha.247
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/CHANGELOG.md +23 -0
- package/package.json +6 -6
- package/src/components/Api/Api_Naming.res +58 -0
- package/src/components/Api/Api_Naming.res.mjs +44 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +61 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +44 -3
- package/src/components/Api/GraphQL_SchemaInspector.res.mjs +2 -2
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +88 -8
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +86 -28
- package/src/components/Dcb/Dcb_Builder.res +3 -0
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +108 -12
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +89 -22
- package/src/plugin/component/Plugin_Builder.res +2 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +141 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +109 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
6
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
@@ -17,7 +18,8 @@ let todoStatusSchema = Sury.union([
|
|
|
17
18
|
Sury.literal("Pending"),
|
|
18
19
|
Sury.literal("Processing"),
|
|
19
20
|
Sury.literal("Completed"),
|
|
20
|
-
Sury.literal("Failed")
|
|
21
|
+
Sury.literal("Failed"),
|
|
22
|
+
Sury.literal("Abandoned")
|
|
21
23
|
]);
|
|
22
24
|
|
|
23
25
|
let todoRowSchema = Sury.$schema(s => ({
|
|
@@ -26,7 +28,8 @@ let todoRowSchema = Sury.$schema(s => ({
|
|
|
26
28
|
createdAt: s.m(Sury.string),
|
|
27
29
|
processedAt: s.m(Sury.$option(Sury.string)),
|
|
28
30
|
completedAt: s.m(Sury.$option(Sury.string)),
|
|
29
|
-
retryCount: s.m(Sury.int)
|
|
31
|
+
retryCount: s.m(Sury.int),
|
|
32
|
+
maxRetries: s.m(Sury.$option(Sury.int))
|
|
30
33
|
}));
|
|
31
34
|
|
|
32
35
|
function todoRowSchemaFor(itemSchema) {
|
|
@@ -36,7 +39,8 @@ function todoRowSchemaFor(itemSchema) {
|
|
|
36
39
|
createdAt: s.m(Sury.string),
|
|
37
40
|
processedAt: s.m(Sury.$option(Sury.string)),
|
|
38
41
|
completedAt: s.m(Sury.$option(Sury.string)),
|
|
39
|
-
retryCount: s.m(Sury.int)
|
|
42
|
+
retryCount: s.m(Sury.int),
|
|
43
|
+
maxRetries: s.m(Sury.$option(Sury.int))
|
|
40
44
|
}));
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -44,6 +48,13 @@ function Make(Spec) {
|
|
|
44
48
|
return Automation => {
|
|
45
49
|
let todoItems = {};
|
|
46
50
|
let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
51
|
+
let afterFailedAttempt = row => {
|
|
52
|
+
let retryCount = row.retryCount + 1 | 0;
|
|
53
|
+
let newrecord = {...row};
|
|
54
|
+
newrecord.retryCount = retryCount;
|
|
55
|
+
newrecord.status = retryCount >= Spec.maxRetries ? "Abandoned" : "Failed";
|
|
56
|
+
return newrecord;
|
|
57
|
+
};
|
|
47
58
|
let dispatches = Automation.mappings.map(M => {
|
|
48
59
|
let decoder = DcbDecode$Reventless.makeDecoder(M.sourceEventSchema);
|
|
49
60
|
let handle = (json, ctx) => {
|
|
@@ -61,11 +72,13 @@ function Make(Spec) {
|
|
|
61
72
|
}
|
|
62
73
|
let row_item = Util_Sury$Reventless.toJson(param[1], Spec.todoItemSchema);
|
|
63
74
|
let row_createdAt = new Date().toISOString();
|
|
75
|
+
let row_maxRetries = Spec.maxRetries;
|
|
64
76
|
let row = {
|
|
65
77
|
item: row_item,
|
|
66
78
|
status: "Pending",
|
|
67
79
|
createdAt: row_createdAt,
|
|
68
|
-
retryCount: 0
|
|
80
|
+
retryCount: 0,
|
|
81
|
+
maxRetries: row_maxRetries
|
|
69
82
|
};
|
|
70
83
|
todoItems[id] = row;
|
|
71
84
|
});
|
|
@@ -98,6 +111,18 @@ function Make(Spec) {
|
|
|
98
111
|
});
|
|
99
112
|
};
|
|
100
113
|
let phase2 = async publishJsons => {
|
|
114
|
+
let newlyAbandoned = [];
|
|
115
|
+
Object.entries(todoItems).forEach(param => {
|
|
116
|
+
let row = param[1];
|
|
117
|
+
if (!(row.status === "Failed" && row.retryCount >= Spec.maxRetries)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let id = param[0];
|
|
121
|
+
let newrecord = {...row};
|
|
122
|
+
newrecord.status = "Abandoned";
|
|
123
|
+
todoItems[id] = newrecord;
|
|
124
|
+
newlyAbandoned.push(id);
|
|
125
|
+
});
|
|
101
126
|
let pending = Object.entries(todoItems).filter(param => {
|
|
102
127
|
let row = param[1];
|
|
103
128
|
if (row.status === "Pending") {
|
|
@@ -138,10 +163,11 @@ function Make(Spec) {
|
|
|
138
163
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
139
164
|
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
140
165
|
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `failed to encode command: ` + errMsg));
|
|
141
|
-
let
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
let updated = afterFailedAttempt(row);
|
|
167
|
+
todoItems[id] = updated;
|
|
168
|
+
if (updated.status === "Abandoned") {
|
|
169
|
+
newlyAbandoned.push(id);
|
|
170
|
+
}
|
|
145
171
|
commandJson = undefined;
|
|
146
172
|
}
|
|
147
173
|
Stdlib_Option.forEach(commandJson, commandJson => {
|
|
@@ -155,32 +181,64 @@ function Make(Spec) {
|
|
|
155
181
|
});
|
|
156
182
|
});
|
|
157
183
|
});
|
|
158
|
-
if (commands.length
|
|
159
|
-
|
|
184
|
+
if (commands.length !== 0) {
|
|
185
|
+
try {
|
|
186
|
+
await publishJsons(commands);
|
|
187
|
+
} catch (raw_exn) {
|
|
188
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
189
|
+
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
190
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `failed to publish commands: ` + errMsg));
|
|
191
|
+
pending.forEach(param => {
|
|
192
|
+
let id = param[0];
|
|
193
|
+
let current = todoItems[id];
|
|
194
|
+
if (current === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (current.status !== "Processing") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let updated = afterFailedAttempt(param[1]);
|
|
201
|
+
todoItems[id] = updated;
|
|
202
|
+
if (updated.status === "Abandoned") {
|
|
203
|
+
newlyAbandoned.push(id);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
160
208
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
209
|
+
let abandonmentCommands = Stdlib_Array.filterMap(newlyAbandoned, id => Stdlib_Option.flatMap(todoItems[id], row => {
|
|
210
|
+
let item;
|
|
211
|
+
try {
|
|
212
|
+
item = Util_Sury$Reventless.fromJson(row.item, Spec.todoItemSchema);
|
|
213
|
+
} catch (exn) {
|
|
214
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `retries exhausted for ` + id + `, and its item could not be decoded to announce it`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
return Stdlib_Option.flatMap(Automation.onExhausted(id, item), param => {
|
|
218
|
+
let commandJson;
|
|
219
|
+
try {
|
|
220
|
+
commandJson = Util_Sury$Reventless.toJson(param[1], Spec.commandSchema);
|
|
221
|
+
} catch (exn) {
|
|
222
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `retries exhausted for ` + id + `, and the abandonment command could not be encoded`));
|
|
175
223
|
return;
|
|
176
224
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
225
|
+
return {
|
|
226
|
+
id: param[0],
|
|
227
|
+
meta: makeMeta(),
|
|
228
|
+
commandJson: commandJson
|
|
229
|
+
};
|
|
181
230
|
});
|
|
231
|
+
}));
|
|
232
|
+
if (abandonmentCommands.length === 0) {
|
|
182
233
|
return;
|
|
183
234
|
}
|
|
235
|
+
try {
|
|
236
|
+
return await publishJsons(abandonmentCommands);
|
|
237
|
+
} catch (raw_exn$1) {
|
|
238
|
+
let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
|
|
239
|
+
let errMsg$1 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "unknown");
|
|
240
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `failed to publish abandonment commands: ` + errMsg$1));
|
|
241
|
+
}
|
|
184
242
|
};
|
|
185
243
|
return {
|
|
186
244
|
Spec: Spec,
|
|
@@ -1095,6 +1095,8 @@ module Make = (
|
|
|
1095
1095
|
let subIdField = V.Spec.subIdConfig->Option.map(c => c.subIdField)
|
|
1096
1096
|
let indexes = V.Spec.config.indexes
|
|
1097
1097
|
let indexQueries = if indexes->Array.length > 0 {Some(indexes)} else {None}
|
|
1098
|
+
let resolved = Api_Naming.resolvedFieldsOfConfig(~plugin=name, V.Spec.config)
|
|
1099
|
+
let resolvedFields = if resolved->Array.length > 0 {Some(resolved)} else {None}
|
|
1098
1100
|
{
|
|
1099
1101
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1100
1102
|
listFieldName: qn.listFieldName,
|
|
@@ -1107,6 +1109,7 @@ module Make = (
|
|
|
1107
1109
|
connectionSpec: true,
|
|
1108
1110
|
subIdField: ?subIdField,
|
|
1109
1111
|
indexQueries: ?indexQueries,
|
|
1112
|
+
resolvedFields: ?resolvedFields,
|
|
1110
1113
|
}
|
|
1111
1114
|
})
|
|
1112
1115
|
|
|
@@ -603,6 +603,8 @@ function Make(DcbEventLogStorage) {
|
|
|
603
603
|
let subIdField = Stdlib_Option.map(V.Spec.subIdConfig, c => c.subIdField);
|
|
604
604
|
let indexes = V.Spec.config.indexes;
|
|
605
605
|
let indexQueries = indexes.length !== 0 ? indexes : undefined;
|
|
606
|
+
let resolved = Api_Naming$ReventlessCore.resolvedFieldsOfConfig(name, V.Spec.config);
|
|
607
|
+
let resolvedFields = resolved.length !== 0 ? resolved : undefined;
|
|
606
608
|
return {
|
|
607
609
|
singleFieldName: qn.singleFieldName,
|
|
608
610
|
listFieldName: qn.listFieldName,
|
|
@@ -614,7 +616,8 @@ function Make(DcbEventLogStorage) {
|
|
|
614
616
|
includeIdParam: qn.includeIdParam,
|
|
615
617
|
connectionSpec: true,
|
|
616
618
|
subIdField: subIdField,
|
|
617
|
-
indexQueries: indexQueries
|
|
619
|
+
indexQueries: indexQueries,
|
|
620
|
+
resolvedFields: resolvedFields
|
|
618
621
|
};
|
|
619
622
|
});
|
|
620
623
|
let automationEntries = automationSlices.map(A => {
|
|
@@ -8,7 +8,12 @@ type todoStatus =
|
|
|
8
8
|
| Pending
|
|
9
9
|
| Processing
|
|
10
10
|
| Completed
|
|
11
|
+
/** Failed and will be tried again. */
|
|
11
12
|
| Failed
|
|
13
|
+
/** Failed for the last time — the retry budget is spent and no pass will pick
|
|
14
|
+
this row up again. Distinct from `Failed` because the two are opposite
|
|
15
|
+
instructions to whoever is reading: wait, or intervene. */
|
|
16
|
+
| Abandoned
|
|
12
17
|
|
|
13
18
|
@schema
|
|
14
19
|
type todoRow = {
|
|
@@ -18,6 +23,11 @@ type todoRow = {
|
|
|
18
23
|
processedAt?: string,
|
|
19
24
|
completedAt?: string,
|
|
20
25
|
retryCount: int,
|
|
26
|
+
/** The ceiling `retryCount` is racing, copied from the Spec when the row is
|
|
27
|
+
created. Optional because rows written before this field existed are
|
|
28
|
+
rehydrated through `S.parseOrThrow`, and a required field would fail that
|
|
29
|
+
decode — silently, since the restore swallows it per row. */
|
|
30
|
+
maxRetries?: int,
|
|
21
31
|
lastError?: string,
|
|
22
32
|
}
|
|
23
33
|
|
|
@@ -50,6 +60,7 @@ let todoRowSchemaFor = (itemSchema: S.t<'item>): S.t<todoRow> =>
|
|
|
50
60
|
processedAt: ?s.matches(S.option(S.string)),
|
|
51
61
|
completedAt: ?s.matches(S.option(S.string)),
|
|
52
62
|
retryCount: s.matches(S.int),
|
|
63
|
+
maxRetries: ?s.matches(S.option(S.int)),
|
|
53
64
|
lastError: ?s.matches(S.option(S.string)),
|
|
54
65
|
})
|
|
55
66
|
|
|
@@ -135,6 +146,61 @@ module Make = (
|
|
|
135
146
|
~service=Spec.targetName->Option.getOr(`OutboundTranslationSlice:${Spec.name}`),
|
|
136
147
|
)
|
|
137
148
|
|
|
149
|
+
/** Record a failed attempt. The row is marked `Abandoned` at the moment the
|
|
150
|
+
budget runs out rather than being left to fall out of the next pass's
|
|
151
|
+
filter, so a reader never has to compare `retryCount` against a constant
|
|
152
|
+
that lives only in this Spec. */
|
|
153
|
+
let afterFailedAttempt = (row: todoRow, ~lastError: option<string>=?): todoRow => {
|
|
154
|
+
let retryCount = row.retryCount + 1
|
|
155
|
+
{
|
|
156
|
+
...row,
|
|
157
|
+
status: retryCount >= Spec.maxRetries ? Abandoned : Failed,
|
|
158
|
+
retryCount,
|
|
159
|
+
lastError: ?switch lastError {
|
|
160
|
+
| Some(_) as given => given
|
|
161
|
+
| None => row.lastError
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Ask the slice whether abandonment is worth telling the domain about, and
|
|
167
|
+
publish what it says. Failing to publish here cannot mark the row for retry
|
|
168
|
+
— the budget is what ran out — so the loss is logged and the row stays
|
|
169
|
+
`Abandoned`. */
|
|
170
|
+
let announceAbandonment = async (
|
|
171
|
+
~id: string,
|
|
172
|
+
~row: todoRow,
|
|
173
|
+
~publishJsons: ReventlessInfra.CommandTopic.publishJsons,
|
|
174
|
+
) =>
|
|
175
|
+
switch row.item->Reventless.Util_Sury.fromJson(Spec.outboundItemSchema) {
|
|
176
|
+
| item =>
|
|
177
|
+
switch Translation.onExhausted(id, item, ~lastError=row.lastError) {
|
|
178
|
+
| None => ()
|
|
179
|
+
| Some((targetId, cmd)) =>
|
|
180
|
+
try {
|
|
181
|
+
let msg: Reventless.Message.commandJson = {
|
|
182
|
+
id: targetId,
|
|
183
|
+
meta: makeMeta(),
|
|
184
|
+
commandJson: cmd->Reventless.Util_Sury.toJson(Spec.inboundCommandSchema),
|
|
185
|
+
}
|
|
186
|
+
await publishJsons([msg])
|
|
187
|
+
} catch {
|
|
188
|
+
| exn =>
|
|
189
|
+
let errMsg =
|
|
190
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
|
|
191
|
+
EffectLogger.logError(
|
|
192
|
+
~comp=`OutboundTranslationSlice(${Spec.name})`,
|
|
193
|
+
`retries exhausted for ${id}, and the abandonment command could not be published: ${errMsg}`,
|
|
194
|
+
)->Effect.runSync
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
| exception _ =>
|
|
198
|
+
EffectLogger.logError(
|
|
199
|
+
~comp=`OutboundTranslationSlice(${Spec.name})`,
|
|
200
|
+
`retries exhausted for ${id}, and its item could not be decoded to announce it`,
|
|
201
|
+
)->Effect.runSync
|
|
202
|
+
}
|
|
203
|
+
|
|
138
204
|
let phase1 = (events: array<(string, Spec.consumedEvent)>) => {
|
|
139
205
|
events->Array.forEach(((sourceId, event)) => {
|
|
140
206
|
// Collect new outbound items
|
|
@@ -147,6 +213,7 @@ module Make = (
|
|
|
147
213
|
status: Pending,
|
|
148
214
|
createdAt: now(),
|
|
149
215
|
retryCount: 0,
|
|
216
|
+
maxRetries: Spec.maxRetries,
|
|
150
217
|
}
|
|
151
218
|
todoItems->Dict.set(id, row)
|
|
152
219
|
}
|
|
@@ -158,6 +225,23 @@ module Make = (
|
|
|
158
225
|
publishJsons: ReventlessInfra.CommandTopic.publishJsons,
|
|
159
226
|
~capabilities: Reventless.Capabilities.t,
|
|
160
227
|
) => {
|
|
228
|
+
// Ids that ran out of budget during this pass, announced once at the end so
|
|
229
|
+
// one item's abandonment cannot interleave with another's attempt.
|
|
230
|
+
let newlyAbandoned: array<string> = []
|
|
231
|
+
|
|
232
|
+
// A `Failed` row at or past the ceiling is one an older build left stranded,
|
|
233
|
+
// or one whose Spec lowered `maxRetries` under it. Either way it is abandoned
|
|
234
|
+
// in fact, so say so rather than letting it sit in a status that promises a
|
|
235
|
+
// retry no pass will make.
|
|
236
|
+
todoItems
|
|
237
|
+
->Dict.toArray
|
|
238
|
+
->Array.forEach(((id, row)) =>
|
|
239
|
+
if row.status == Failed && row.retryCount >= Spec.maxRetries {
|
|
240
|
+
todoItems->Dict.set(id, {...row, status: Abandoned})
|
|
241
|
+
newlyAbandoned->Array.push(id)
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
161
245
|
let pending =
|
|
162
246
|
todoItems
|
|
163
247
|
->Dict.toArray
|
|
@@ -229,16 +313,18 @@ module Make = (
|
|
|
229
313
|
~comp=`OutboundTranslationSlice(${Spec.name})`,
|
|
230
314
|
`failed to publish command: ${errMsg}`,
|
|
231
315
|
)->Effect.runSync
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
316
|
+
let updated = row->afterFailedAttempt(~lastError=errMsg)
|
|
317
|
+
todoItems->Dict.set(id, updated)
|
|
318
|
+
if updated.status == Abandoned {
|
|
319
|
+
newlyAbandoned->Array.push(id)
|
|
320
|
+
}
|
|
236
321
|
}
|
|
237
322
|
| None =>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
323
|
+
let updated = row->afterFailedAttempt
|
|
324
|
+
todoItems->Dict.set(id, updated)
|
|
325
|
+
if updated.status == Abandoned {
|
|
326
|
+
newlyAbandoned->Array.push(id)
|
|
327
|
+
}
|
|
242
328
|
}
|
|
243
329
|
|
|
244
330
|
| Ok(None) =>
|
|
@@ -246,12 +332,22 @@ module Make = (
|
|
|
246
332
|
todoItems->Dict.set(id, {...row, status: Completed, completedAt: now()})
|
|
247
333
|
|
|
248
334
|
| Error(msg) =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
335
|
+
let updated = row->afterFailedAttempt(~lastError=msg)
|
|
336
|
+
todoItems->Dict.set(id, updated)
|
|
337
|
+
if updated.status == Abandoned {
|
|
338
|
+
newlyAbandoned->Array.push(id)
|
|
339
|
+
}
|
|
253
340
|
}
|
|
254
341
|
}
|
|
255
342
|
})
|
|
343
|
+
|
|
344
|
+
// The row is already `Abandoned`; this only asks whether the domain hears.
|
|
345
|
+
let _ = await newlyAbandoned->Array.reduce(Promise.resolve(), async (prev, id) => {
|
|
346
|
+
let _ = await prev
|
|
347
|
+
switch todoItems->Dict.get(id) {
|
|
348
|
+
| Some(row) => await announceAbandonment(~id, ~row, ~publishJsons)
|
|
349
|
+
| None => ()
|
|
350
|
+
}
|
|
351
|
+
})
|
|
256
352
|
}
|
|
257
353
|
}
|
|
@@ -15,7 +15,8 @@ let todoStatusSchema = Sury.union([
|
|
|
15
15
|
Sury.literal("Pending"),
|
|
16
16
|
Sury.literal("Processing"),
|
|
17
17
|
Sury.literal("Completed"),
|
|
18
|
-
Sury.literal("Failed")
|
|
18
|
+
Sury.literal("Failed"),
|
|
19
|
+
Sury.literal("Abandoned")
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
let todoRowSchema = Sury.$schema(s => ({
|
|
@@ -25,6 +26,7 @@ let todoRowSchema = Sury.$schema(s => ({
|
|
|
25
26
|
processedAt: s.m(Sury.$option(Sury.string)),
|
|
26
27
|
completedAt: s.m(Sury.$option(Sury.string)),
|
|
27
28
|
retryCount: s.m(Sury.int),
|
|
29
|
+
maxRetries: s.m(Sury.$option(Sury.int)),
|
|
28
30
|
lastError: s.m(Sury.$option(Sury.string))
|
|
29
31
|
}));
|
|
30
32
|
|
|
@@ -36,6 +38,7 @@ function todoRowSchemaFor(itemSchema) {
|
|
|
36
38
|
processedAt: s.m(Sury.$option(Sury.string)),
|
|
37
39
|
completedAt: s.m(Sury.$option(Sury.string)),
|
|
38
40
|
retryCount: s.m(Sury.int),
|
|
41
|
+
maxRetries: s.m(Sury.$option(Sury.int)),
|
|
39
42
|
lastError: s.m(Sury.$option(Sury.string))
|
|
40
43
|
}));
|
|
41
44
|
}
|
|
@@ -44,6 +47,41 @@ function Make(Spec) {
|
|
|
44
47
|
return Translation => {
|
|
45
48
|
let todoItems = {};
|
|
46
49
|
let makeMeta = () => Message$ReventlessCore.generateMeta(Stdlib_Option.getOr(Spec.targetName, `OutboundTranslationSlice:` + Spec.name), undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
50
|
+
let afterFailedAttempt = (row, lastError) => {
|
|
51
|
+
let retryCount = row.retryCount + 1 | 0;
|
|
52
|
+
let newrecord = {...row};
|
|
53
|
+
newrecord.lastError = lastError !== undefined ? lastError : row.lastError;
|
|
54
|
+
newrecord.retryCount = retryCount;
|
|
55
|
+
newrecord.status = retryCount >= Spec.maxRetries ? "Abandoned" : "Failed";
|
|
56
|
+
return newrecord;
|
|
57
|
+
};
|
|
58
|
+
let announceAbandonment = async (id, row, publishJsons) => {
|
|
59
|
+
let item;
|
|
60
|
+
try {
|
|
61
|
+
item = Util_Sury$Reventless.fromJson(row.item, Spec.outboundItemSchema);
|
|
62
|
+
} catch (exn) {
|
|
63
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `retries exhausted for ` + id + `, and its item could not be decoded to announce it`));
|
|
64
|
+
}
|
|
65
|
+
let match = Translation.onExhausted(id, item, row.lastError);
|
|
66
|
+
if (match === undefined) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
let msg_id = match[0];
|
|
71
|
+
let msg_meta = makeMeta();
|
|
72
|
+
let msg_commandJson = Util_Sury$Reventless.toJson(match[1], Spec.inboundCommandSchema);
|
|
73
|
+
let msg = {
|
|
74
|
+
id: msg_id,
|
|
75
|
+
meta: msg_meta,
|
|
76
|
+
commandJson: msg_commandJson
|
|
77
|
+
};
|
|
78
|
+
return await publishJsons([msg]);
|
|
79
|
+
} catch (raw_exn) {
|
|
80
|
+
let exn$1 = Primitive_exceptions.internalToException(raw_exn);
|
|
81
|
+
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "unknown");
|
|
82
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `retries exhausted for ` + id + `, and the abandonment command could not be published: ` + errMsg));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
47
85
|
let phase1 = events => {
|
|
48
86
|
events.forEach(param => {
|
|
49
87
|
Translation.collect(param[1], param[0]).forEach(param => {
|
|
@@ -54,17 +92,31 @@ function Make(Spec) {
|
|
|
54
92
|
}
|
|
55
93
|
let row_item = Util_Sury$Reventless.toJson(param[1], Spec.outboundItemSchema);
|
|
56
94
|
let row_createdAt = new Date().toISOString();
|
|
95
|
+
let row_maxRetries = Spec.maxRetries;
|
|
57
96
|
let row = {
|
|
58
97
|
item: row_item,
|
|
59
98
|
status: "Pending",
|
|
60
99
|
createdAt: row_createdAt,
|
|
61
|
-
retryCount: 0
|
|
100
|
+
retryCount: 0,
|
|
101
|
+
maxRetries: row_maxRetries
|
|
62
102
|
};
|
|
63
103
|
todoItems[id] = row;
|
|
64
104
|
});
|
|
65
105
|
});
|
|
66
106
|
};
|
|
67
107
|
let phase2 = async (publishJsons, capabilities) => {
|
|
108
|
+
let newlyAbandoned = [];
|
|
109
|
+
Object.entries(todoItems).forEach(param => {
|
|
110
|
+
let row = param[1];
|
|
111
|
+
if (!(row.status === "Failed" && row.retryCount >= Spec.maxRetries)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let id = param[0];
|
|
115
|
+
let newrecord = {...row};
|
|
116
|
+
newrecord.status = "Abandoned";
|
|
117
|
+
todoItems[id] = newrecord;
|
|
118
|
+
newlyAbandoned.push(id);
|
|
119
|
+
});
|
|
68
120
|
let pending = Object.entries(todoItems).filter(param => {
|
|
69
121
|
let row = param[1];
|
|
70
122
|
if (row.status === "Pending") {
|
|
@@ -137,33 +189,48 @@ function Make(Spec) {
|
|
|
137
189
|
let exn$3 = Primitive_exceptions.internalToException(raw_exn$3);
|
|
138
190
|
let errMsg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$3), Stdlib_JsExn.message), "unknown");
|
|
139
191
|
Effect.runSync(EffectLogger$ReventlessCore.logError(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `failed to publish command: ` + errMsg$2));
|
|
140
|
-
let
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
192
|
+
let updated = afterFailedAttempt(row, errMsg$2);
|
|
193
|
+
todoItems[id] = updated;
|
|
194
|
+
if (updated.status === "Abandoned") {
|
|
195
|
+
newlyAbandoned.push(id);
|
|
196
|
+
return;
|
|
197
|
+
} else {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
145
200
|
}
|
|
146
201
|
} else {
|
|
147
|
-
let
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
202
|
+
let updated$1 = afterFailedAttempt(row, undefined);
|
|
203
|
+
todoItems[id] = updated$1;
|
|
204
|
+
if (updated$1.status === "Abandoned") {
|
|
205
|
+
newlyAbandoned.push(id);
|
|
206
|
+
return;
|
|
207
|
+
} else {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
152
210
|
}
|
|
153
211
|
} else {
|
|
154
|
-
let newrecord$
|
|
155
|
-
newrecord$
|
|
156
|
-
newrecord$
|
|
157
|
-
todoItems[id] = newrecord$
|
|
212
|
+
let newrecord$2 = {...row};
|
|
213
|
+
newrecord$2.completedAt = new Date().toISOString();
|
|
214
|
+
newrecord$2.status = "Completed";
|
|
215
|
+
todoItems[id] = newrecord$2;
|
|
158
216
|
return;
|
|
159
217
|
}
|
|
160
218
|
} else {
|
|
161
|
-
let
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
219
|
+
let updated$2 = afterFailedAttempt(row, result._0);
|
|
220
|
+
todoItems[id] = updated$2;
|
|
221
|
+
if (updated$2.status === "Abandoned") {
|
|
222
|
+
newlyAbandoned.push(id);
|
|
223
|
+
return;
|
|
224
|
+
} else {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
await Stdlib_Array.reduce(newlyAbandoned, Promise.resolve(), async (prev, id) => {
|
|
230
|
+
await prev;
|
|
231
|
+
let row = todoItems[id];
|
|
232
|
+
if (row !== undefined) {
|
|
233
|
+
return await announceAbandonment(id, row, publishJsons);
|
|
167
234
|
}
|
|
168
235
|
});
|
|
169
236
|
};
|
|
@@ -255,6 +255,7 @@ module Make = (
|
|
|
255
255
|
// its SDL did not declare. The DCB and admin entries have populated this
|
|
256
256
|
// all along; the omission here was the asymmetry, not the rule.
|
|
257
257
|
let indexes = R.Spec.config.indexes
|
|
258
|
+
let resolvedFields = Api_Naming.resolvedFieldsOfConfig(~plugin=name, R.Spec.config)
|
|
258
259
|
{
|
|
259
260
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
260
261
|
listFieldName: qn.listFieldName,
|
|
@@ -265,6 +266,7 @@ module Make = (
|
|
|
265
266
|
connectionSpec: true,
|
|
266
267
|
subIdField: ?subIdField,
|
|
267
268
|
indexQueries: ?(indexes->Array.length > 0 ? Some(indexes) : None),
|
|
269
|
+
resolvedFields: ?(resolvedFields->Array.length > 0 ? Some(resolvedFields) : None),
|
|
268
270
|
}
|
|
269
271
|
})
|
|
270
272
|
|
|
@@ -196,6 +196,7 @@ function Make(Spec) {
|
|
|
196
196
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(extra$1, R.Spec.name, undefined);
|
|
197
197
|
let subIdField = Stdlib_Option.map(R.Spec.subIdConfig, c => c.subIdField);
|
|
198
198
|
let indexes = R.Spec.config.indexes;
|
|
199
|
+
let resolvedFields = Api_Naming$ReventlessCore.resolvedFieldsOfConfig(extra$1, R.Spec.config);
|
|
199
200
|
return {
|
|
200
201
|
singleFieldName: qn.singleFieldName,
|
|
201
202
|
listFieldName: qn.listFieldName,
|
|
@@ -205,7 +206,8 @@ function Make(Spec) {
|
|
|
205
206
|
permission: R.Spec.authorization,
|
|
206
207
|
connectionSpec: true,
|
|
207
208
|
subIdField: subIdField,
|
|
208
|
-
indexQueries: indexes.length !== 0 ? indexes : undefined
|
|
209
|
+
indexQueries: indexes.length !== 0 ? indexes : undefined,
|
|
210
|
+
resolvedFields: resolvedFields.length !== 0 ? resolvedFields : undefined
|
|
209
211
|
};
|
|
210
212
|
});
|
|
211
213
|
let queryEntries = queryEntriesFromReadModels.concat(dcbResult.queryEntries);
|