@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.
@@ -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 newrecord$1 = {...row};
142
- newrecord$1.retryCount = row.retryCount + 1 | 0;
143
- newrecord$1.status = "Failed";
144
- todoItems[id] = newrecord$1;
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 === 0) {
159
- return;
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
- try {
162
- return await publishJsons(commands);
163
- } catch (raw_exn) {
164
- let exn = Primitive_exceptions.internalToException(raw_exn);
165
- let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
166
- Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `failed to publish commands: ` + errMsg));
167
- pending.forEach(param => {
168
- let row = param[1];
169
- let id = param[0];
170
- let current = todoItems[id];
171
- if (current === undefined) {
172
- return;
173
- }
174
- if (current.status !== "Processing") {
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
- let newrecord = {...row};
178
- newrecord.retryCount = row.retryCount + 1 | 0;
179
- newrecord.status = "Failed";
180
- todoItems[id] = newrecord;
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
- todoItems->Dict.set(
233
- id,
234
- {...row, status: Failed, retryCount: row.retryCount + 1},
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
- todoItems->Dict.set(
239
- id,
240
- {...row, status: Failed, retryCount: row.retryCount + 1},
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
- todoItems->Dict.set(
250
- id,
251
- {...row, status: Failed, retryCount: row.retryCount + 1, lastError: msg},
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 newrecord$2 = {...row};
141
- newrecord$2.retryCount = row.retryCount + 1 | 0;
142
- newrecord$2.status = "Failed";
143
- todoItems[id] = newrecord$2;
144
- return;
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 newrecord$3 = {...row};
148
- newrecord$3.retryCount = row.retryCount + 1 | 0;
149
- newrecord$3.status = "Failed";
150
- todoItems[id] = newrecord$3;
151
- return;
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$4 = {...row};
155
- newrecord$4.completedAt = new Date().toISOString();
156
- newrecord$4.status = "Completed";
157
- todoItems[id] = newrecord$4;
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 newrecord$5 = {...row};
162
- newrecord$5.lastError = result._0;
163
- newrecord$5.retryCount = row.retryCount + 1 | 0;
164
- newrecord$5.status = "Failed";
165
- todoItems[id] = newrecord$5;
166
- return;
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);