@reventlessdev/reventless-aws 3.0.0-alpha.263 → 3.0.0-alpha.265

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +7 -7
  3. package/src/Platform.res +37 -6
  4. package/src/Platform.res.mjs +21 -6
  5. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +6 -1
  6. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +4 -1
  7. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +5 -0
  8. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +3 -0
  9. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +6 -1
  10. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +4 -1
  11. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +6 -1
  12. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +4 -1
  13. package/src/adapter/CommandTopic/CommandTopicRegistry.res +50 -0
  14. package/src/adapter/CommandTopic/CommandTopicRegistry.res.mjs +26 -0
  15. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -3
  16. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +230 -29
  17. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +124 -9
  18. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +188 -3
  19. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +107 -6
  20. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  21. package/src/components/OutboundTranslationSlice_Builder.res +20 -0
  22. package/src/components/OutboundTranslationSlice_Builder.res.mjs +12 -2
  23. package/src/plugin/runtime/PluginRuntime_Builder.res +26 -0
  24. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
  25. package/tests/AutomationSliceEntryPoint_OpsTest.res +44 -3
  26. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +65 -1
@@ -27,6 +27,10 @@ type handlerEntry = {
27
27
  callbackType: string,
28
28
  queryDbTableName: string,
29
29
  dcbQueueUrl: string,
30
+ /** Whether `dcbQueueUrl` is a FIFO queue. Absent in a config written before
31
+ the flavor travelled with the URL, where `false` — a standard queue — is
32
+ what these command topics actually are. */
33
+ commandQueueIsFifo: option<bool>,
30
34
  sourceUrn: string,
31
35
  context: option<Reventless.AutomationSlice.context>,
32
36
  }
@@ -53,6 +57,7 @@ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
53
57
  callbackType: h->strOf("callbackType")->Option.getOr("automation"),
54
58
  queryDbTableName: h->strOf("queryDbTableName")->Option.getOr(""),
55
59
  dcbQueueUrl: h->strOf("dcbQueueUrl")->Option.getOr(""),
60
+ commandQueueIsFifo: h->Dict.get("commandQueueIsFifo")->Option.flatMap(JSON.Decode.bool),
56
61
  sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
57
62
  context: h->Dict.get("context")->Option.flatMap(decodeContext),
58
63
  })
@@ -88,9 +93,13 @@ let defaultContext = (sliceName: string): Reventless.AutomationSlice.context =>
88
93
 
89
94
  // ── Shared plumbing ─────────────────────────────────────────────────────────
90
95
 
91
- let makePublishJsons = (queueUrl: string): ReventlessCore.CommandTopic.publishJsons => {
96
+ // The flavor comes from the handler config rather than being assumed. It used to
97
+ // be hardcoded FIFO while the command topics are standard queues, and SQS rejects
98
+ // the MessageGroupId a FIFO publisher attaches — a failure nothing had reached,
99
+ // because the role had no sqs:SendMessage to get that far.
100
+ let makePublishJsons = (queueUrl: string, ~isFifo: bool): ReventlessCore.CommandTopic.publishJsons => {
92
101
  let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
93
- queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
102
+ queue->CommandTopicChannel_SQS_Runtime.publishJsons(isFifo ? AWS.SQS_FIFO : AWS.SQS)
94
103
  }
95
104
 
96
105
  // Overwrite-sync the in-memory TODO list into the slice's view table. Rows are
@@ -119,6 +128,111 @@ let makeSyncTodoItems = (
119
128
  }
120
129
  }
121
130
 
131
+ // Rehydrate the in-memory TODO list from the slice's view table, once per
132
+ // container.
133
+ //
134
+ // `todoItems` is a module-level dict populated only by phase 1, so before this
135
+ // existed a cold start began with an empty list and every row already persisted
136
+ // was unreachable: `phase2` admits `Failed && retryCount < maxRetries`, but only
137
+ // for rows it can see. A transient failure whose container then recycled was
138
+ // stranded permanently, in a row reading `Failed` that nothing would ever pick
139
+ // up again — while `makeSyncTodoItems` kept faithfully writing it back out.
140
+ //
141
+ // Once per container rather than per invocation: within a container the dict is
142
+ // authoritative and `phase2` already re-attempts every actionable row on each
143
+ // batch, so re-reading would buy nothing and cost a scan each time. The backlog
144
+ // is therefore retried on the first event a new container handles.
145
+ //
146
+ // Memory wins on conflict. A row already in the dict may be mid-flight
147
+ // (`Processing`) or a status this invocation just advanced; the stored copy is
148
+ // by definition no fresher.
149
+ //
150
+ // Only `Pending` and `Failed` are read. `Completed` rows are the bulk of a
151
+ // mature table and are never actionable. Retry-exhausted rows are not excluded —
152
+ // `maxRetries` is Spec-level and not known here — but `phase2` filters them, so
153
+ // they are inert rather than wrong.
154
+ let makeLoadTodoItems = (
155
+ ~queryDbTableName: string,
156
+ ~todoItems: dict<'row>,
157
+ ~rowSchema: S.t<'row>,
158
+ ~comp: string,
159
+ ): (unit => promise<unit>) => {
160
+ let loaded = ref(false)
161
+ async () =>
162
+ if loaded.contents {
163
+ ()
164
+ } else {
165
+ loaded := true
166
+ let params: AwsSdk.DynamoDb.DocumentClient.ScanCommand.input = {
167
+ tableName: queryDbTableName,
168
+ consistentRead: true,
169
+ filterExpression: "#s = :pending OR #s = :failed",
170
+ expressionAttributeNames: [("#s", "status")]->Dict.fromArray,
171
+ expressionAttributeValues: [
172
+ (":pending", "Pending"->JSON.Encode.string),
173
+ (":failed", "Failed"->JSON.Encode.string),
174
+ ]->Dict.fromArray,
175
+ }
176
+ let restored = await Util_DynamoDb_Runtime.scanStream(params)
177
+ ->Stream.runCollect
178
+ ->Effect.map(items =>
179
+ items->Array.reduce(0, (count, item) => {
180
+ let json = item->JSON.stringifyAny->Option.getOr("")->JSON.parseOrThrow
181
+ let id = json->JSON.Decode.object->Option.flatMap(d => d->Dict.get("id"))->Option.flatMap(JSON.Decode.string)
182
+ switch id {
183
+ | Some(id) if todoItems->Dict.get(id)->Option.isNone =>
184
+ switch json->S.parseJsonOrThrow(rowSchema) {
185
+ | row =>
186
+ todoItems->Dict.set(id, row)
187
+ count + 1
188
+ | exception _ => count
189
+ }
190
+ | _ => count
191
+ }
192
+ })
193
+ )
194
+ ->Effect.catchAll(err =>
195
+ ReventlessCore.EffectLogger.logError(
196
+ ~comp,
197
+ `restore: couldn't read pending TODO rows from ${queryDbTableName}: ${DynamoDb_Error.message(
198
+ err,
199
+ )}`,
200
+ )->Effect.map(_ => 0)
201
+ )
202
+ ->Effect.runPromise
203
+ if restored > 0 {
204
+ ReventlessCore.EffectLogger.logInfo(
205
+ ~comp,
206
+ `restore: reloaded ${restored->Int.toString} unfinished TODO row(s) from ${queryDbTableName}`,
207
+ )->Effect.runSync
208
+ }
209
+ }
210
+ }
211
+
212
+ // ── Platform capabilities ───────────────────────────────────────────────────
213
+
214
+ /**
215
+ What this Lambda hands a slice's `translate`.
216
+
217
+ Built here rather than reached for inside the plugin, because a plugin depends on
218
+ `reventless-spec` and cannot name Amazon Location — the whole point of the
219
+ injected record. This module is the AWS side of that seam and calls the SDK
220
+ directly: no proxy hop through the browser's Function URL, and no dependence on a
221
+ public unauthenticated endpoint for an unattended path.
222
+
223
+ Resolved per call rather than captured once, so a Lambda whose configuration is
224
+ updated does not need a cold start to see it. An unset `PLACE_INDEX_NAME` reaches
225
+ `Geocoder_AwsLocation_Backend` as `""`, which answers `Unavailable` — a retryable
226
+ outcome, not a verdict on the address.
227
+ */
228
+ let capabilities = (): Reventless.Capabilities.t => {
229
+ geocode: (~text) =>
230
+ Geocoder_AwsLocation_Backend.search(
231
+ ~indexName=NodeProcess.env->Dict.get("PLACE_INDEX_NAME")->Option.getOr(""),
232
+ ~text,
233
+ ),
234
+ }
235
+
122
236
  // ── Phase-1/phase-2 pipelines ───────────────────────────────────────────────
123
237
  // Both run: collect the batch → phase 1 → sync (so consumers observe Pending
124
238
  // rows even if phase 2 fails) → phase 2 → sync. Unlike the in-process builders
@@ -152,12 +266,14 @@ let makeAutomationJsonEventsHandler = (
152
266
  ~callback: automationCallback,
153
267
  ~publishJsons: ReventlessCore.CommandTopic.publishJsons,
154
268
  ~syncTodoItems: unit => promise<unit>,
269
+ ~loadTodoItems: unit => promise<unit>,
155
270
  ): ReventlessCore.EventCollector.jsonEventsHandler =>
156
271
  stream =>
157
272
  stream
158
273
  ->Stream.runCollect
159
274
  ->Effect.flatMap(jsons =>
160
275
  Effect.promise(async () => {
276
+ await loadTodoItems()
161
277
  callback.phase1(jsons, context)
162
278
  await syncTodoItems()
163
279
  await callback.phase2(publishJsons)
@@ -174,6 +290,7 @@ let makeOutboundJsonEventsHandler = (
174
290
  ~callback: outboundCallback<'event>,
175
291
  ~publishJsons: ReventlessCore.CommandTopic.publishJsons,
176
292
  ~syncTodoItems: unit => promise<unit>,
293
+ ~loadTodoItems: unit => promise<unit>,
177
294
  ): ReventlessCore.EventCollector.jsonEventsHandler => {
178
295
  let decoder = Reventless.DcbDecode.makeDecoder(consumedEventSchema)
179
296
  stream =>
@@ -202,9 +319,10 @@ let makeOutboundJsonEventsHandler = (
202
319
  ->Stream.runCollect
203
320
  ->Effect.flatMap(events =>
204
321
  Effect.promise(async () => {
322
+ await loadTodoItems()
205
323
  callback.phase1(events)
206
324
  await syncTodoItems()
207
- await callback.phase2(publishJsons)
325
+ await callback.phase2(publishJsons, ~capabilities=capabilities())
208
326
  await syncTodoItems()
209
327
  })
210
328
  )
@@ -214,24 +332,62 @@ let makeOutboundJsonEventsHandler = (
214
332
  // Comp matches what the slice's own callback logs under, so a filter catches
215
333
  // both the framework's lines and the application handler's.
216
334
 
335
+ /**
336
+ A built slice, in the two ways the Lambda drives it.
337
+
338
+ `registered` handles a stream batch. `sweep` runs the TODO backlog with no event
339
+ to trigger it — the scheduled path, and the reason `translatePending` exists on
340
+ the in-process builder. Both close over the *same* publish/sync/load closures, so
341
+ a sweep landing on a container that has already served a batch reuses the load it
342
+ did then, and one landing cold does the reload itself.
343
+ */
344
+ type builtSlice = {
345
+ registered: StreamRoutedEntryPoint_Ops.registeredHandler,
346
+ sweep: unit => promise<unit>,
347
+ comp: string,
348
+ }
349
+
217
350
  let makeAutomationRegisteredHandler = (
218
351
  entry: handlerEntry,
219
352
  ~sliceName: string,
220
353
  ~callback: automationCallback,
221
- ): StreamRoutedEntryPoint_Ops.registeredHandler => {
222
- handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
223
- makeAutomationJsonEventsHandler(
224
- ~context=entry.context->Option.getOr(defaultContext(sliceName)),
225
- ~callback,
226
- ~publishJsons=makePublishJsons(entry.dcbQueueUrl),
227
- ~syncTodoItems=makeSyncTodoItems(
228
- ~queryDbTableName=entry.queryDbTableName,
229
- ~todoItems=callback.todoItems,
230
- ~rowSchema=ReventlessCore.AutomationSlice_Callback.todoRowSchema,
354
+ ): builtSlice => {
355
+ let comp = `AutomationSlice(${sliceName})`
356
+ let publishJsons = makePublishJsons(
357
+ entry.dcbQueueUrl,
358
+ ~isFifo=entry.commandQueueIsFifo->Option.getOr(false),
359
+ )
360
+ let syncTodoItems = makeSyncTodoItems(
361
+ ~queryDbTableName=entry.queryDbTableName,
362
+ ~todoItems=callback.todoItems,
363
+ ~rowSchema=ReventlessCore.AutomationSlice_Callback.todoRowSchema,
364
+ )
365
+ let loadTodoItems = makeLoadTodoItems(
366
+ ~queryDbTableName=entry.queryDbTableName,
367
+ ~todoItems=callback.todoItems,
368
+ ~rowSchema=ReventlessCore.AutomationSlice_Callback.todoRowSchema,
369
+ ~comp,
370
+ )
371
+ {
372
+ registered: {
373
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
374
+ makeAutomationJsonEventsHandler(
375
+ ~context=entry.context->Option.getOr(defaultContext(sliceName)),
376
+ ~callback,
377
+ ~publishJsons,
378
+ ~syncTodoItems,
379
+ ~loadTodoItems,
380
+ ),
231
381
  ),
232
- ),
233
- ),
234
- comp: `AutomationSlice(${sliceName})`,
382
+ comp,
383
+ },
384
+ sweep: async () => {
385
+ await loadTodoItems()
386
+ await callback.phase2(publishJsons)
387
+ await syncTodoItems()
388
+ },
389
+ comp,
390
+ }
235
391
  }
236
392
 
237
393
  let makeOutboundRegisteredHandler = (
@@ -239,18 +395,63 @@ let makeOutboundRegisteredHandler = (
239
395
  ~sliceName: string,
240
396
  ~consumedEventSchema: S.t<'event>,
241
397
  ~callback: outboundCallback<'event>,
242
- ): StreamRoutedEntryPoint_Ops.registeredHandler => {
243
- handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
244
- makeOutboundJsonEventsHandler(
245
- ~consumedEventSchema,
246
- ~callback,
247
- ~publishJsons=makePublishJsons(entry.dcbQueueUrl),
248
- ~syncTodoItems=makeSyncTodoItems(
249
- ~queryDbTableName=entry.queryDbTableName,
250
- ~todoItems=callback.todoItems,
251
- ~rowSchema=ReventlessCore.OutboundTranslationSlice_Callback.todoRowSchema,
398
+ ): builtSlice => {
399
+ let comp = `OutboundTranslationSlice(${sliceName})`
400
+ let publishJsons = makePublishJsons(
401
+ entry.dcbQueueUrl,
402
+ ~isFifo=entry.commandQueueIsFifo->Option.getOr(false),
403
+ )
404
+ let syncTodoItems = makeSyncTodoItems(
405
+ ~queryDbTableName=entry.queryDbTableName,
406
+ ~todoItems=callback.todoItems,
407
+ ~rowSchema=ReventlessCore.OutboundTranslationSlice_Callback.todoRowSchema,
408
+ )
409
+ let loadTodoItems = makeLoadTodoItems(
410
+ ~queryDbTableName=entry.queryDbTableName,
411
+ ~todoItems=callback.todoItems,
412
+ ~rowSchema=ReventlessCore.OutboundTranslationSlice_Callback.todoRowSchema,
413
+ ~comp,
414
+ )
415
+ {
416
+ registered: {
417
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
418
+ makeOutboundJsonEventsHandler(
419
+ ~consumedEventSchema,
420
+ ~callback,
421
+ ~publishJsons,
422
+ ~syncTodoItems,
423
+ ~loadTodoItems,
424
+ ),
252
425
  ),
253
- ),
254
- ),
255
- comp: `OutboundTranslationSlice(${sliceName})`,
426
+ comp,
427
+ },
428
+ sweep: async () => {
429
+ await loadTodoItems()
430
+ await callback.phase2(publishJsons, ~capabilities=capabilities())
431
+ await syncTodoItems()
432
+ },
433
+ comp,
434
+ }
435
+ }
436
+
437
+ /**
438
+ Run every slice's TODO backlog, for a scheduled invocation carrying no records.
439
+
440
+ Sequential rather than concurrent: the slices in one Lambda share its memory and
441
+ its downstream quotas, and a sweep is never latency-critical. A slice that throws
442
+ is logged and does not stop the rest — a sweep that abandoned the remaining
443
+ slices because one geocoder was down would be a worse version of the problem it
444
+ exists to fix.
445
+ */
446
+ let runSweeps = async (slices: array<builtSlice>) => {
447
+ for i in 0 to slices->Array.length - 1 {
448
+ let slice = slices->Array.getUnsafe(i)
449
+ try {
450
+ await slice.sweep()
451
+ } catch {
452
+ | exn =>
453
+ let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
454
+ ReventlessCore.EffectLogger.logError(~comp=slice.comp, `sweep failed: ${msg}`)->Effect.runSync
455
+ }
456
+ }
256
457
  }
@@ -4,14 +4,20 @@ import * as S from "sury/src/S.res.mjs";
4
4
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
5
5
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
9
  import * as Effect from "effect/Effect";
9
10
  import * as Stream$1 from "effect/Stream";
10
11
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
12
  import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
13
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
14
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
15
+ import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
16
+ import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
17
+ import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
13
18
  import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
14
19
  import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
20
+ import * as Geocoder_AwsLocation_Backend$ReventlessAws from "../Geocoder/Geocoder_AwsLocation_Backend.res.mjs";
15
21
  import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
16
22
  import * as QueryDbStorage_DynamoDb_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
17
23
  import * as OutboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
@@ -36,6 +42,7 @@ function decodeEntry(json) {
36
42
  callbackType: Stdlib_Option.getOr(strOf(h, "callbackType"), "automation"),
37
43
  queryDbTableName: Stdlib_Option.getOr(strOf(h, "queryDbTableName"), ""),
38
44
  dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "dcbQueueUrl"), ""),
45
+ commandQueueIsFifo: Stdlib_Option.flatMap(h["commandQueueIsFifo"], Stdlib_JSON.Decode.bool),
39
46
  sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
40
47
  context: Stdlib_Option.flatMap(h["context"], decodeContext)
41
48
  }));
@@ -64,13 +71,13 @@ function defaultContext(sliceName) {
64
71
  };
65
72
  }
66
73
 
67
- function makePublishJsons(queueUrl) {
74
+ function makePublishJsons(queueUrl, isFifo) {
68
75
  let queue = {
69
76
  id: queueUrl,
70
77
  name: queueUrl,
71
78
  arn: ""
72
79
  };
73
- return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO");
80
+ return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, isFifo ? "SQS_FIFO" : "SQS");
74
81
  }
75
82
 
76
83
  function makeSyncTodoItems(queryDbTableName, todoItems, rowSchema) {
@@ -86,8 +93,71 @@ function makeSyncTodoItems(queryDbTableName, todoItems, rowSchema) {
86
93
  };
87
94
  }
88
95
 
89
- function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTodoItems) {
96
+ function makeLoadTodoItems(queryDbTableName, todoItems, rowSchema, comp) {
97
+ let loaded = {
98
+ contents: false
99
+ };
100
+ return async () => {
101
+ if (loaded.contents) {
102
+ return;
103
+ }
104
+ loaded.contents = true;
105
+ let params_ConsistentRead = true;
106
+ let params_ExpressionAttributeNames = Object.fromEntries([[
107
+ "#s",
108
+ "status"
109
+ ]]);
110
+ let params_ExpressionAttributeValues = Object.fromEntries([
111
+ [
112
+ ":pending",
113
+ "Pending"
114
+ ],
115
+ [
116
+ ":failed",
117
+ "Failed"
118
+ ]
119
+ ]);
120
+ let params_FilterExpression = "#s = :pending OR #s = :failed";
121
+ let params = {
122
+ TableName: queryDbTableName,
123
+ ConsistentRead: params_ConsistentRead,
124
+ ExpressionAttributeNames: params_ExpressionAttributeNames,
125
+ ExpressionAttributeValues: params_ExpressionAttributeValues,
126
+ FilterExpression: params_FilterExpression
127
+ };
128
+ let restored = await Effect.runPromise(Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream(params)), items => Stdlib_Array.reduce(items, 0, (count, item) => {
129
+ let json = JSON.parse(Stdlib_Option.getOr(JSON.stringify(item), ""));
130
+ let id = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["id"]), Stdlib_JSON.Decode.string);
131
+ if (id === undefined) {
132
+ return count;
133
+ }
134
+ if (!Stdlib_Option.isNone(todoItems[id])) {
135
+ return count;
136
+ }
137
+ let row;
138
+ try {
139
+ row = S.parseJsonOrThrow(json, rowSchema);
140
+ } catch (exn) {
141
+ return count;
142
+ }
143
+ todoItems[id] = row;
144
+ return count + 1 | 0;
145
+ })), err => Effect.map(EffectLogger$ReventlessCore.logError(comp, undefined, `restore: couldn't read pending TODO rows from ` + queryDbTableName + `: ` + DynamoDb_Error$ReventlessAws.message(err)), () => 0)));
146
+ if (restored > 0) {
147
+ return Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, undefined, `restore: reloaded ` + restored.toString() + ` unfinished TODO row(s) from ` + queryDbTableName));
148
+ }
149
+ };
150
+ }
151
+
152
+ function capabilities() {
153
+ return {
154
+ geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined)
155
+ };
156
+ }
157
+
158
+ function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTodoItems, loadTodoItems) {
90
159
  return stream => Effect.flatMap(Stream.runCollect(stream), jsons => Effect.promise(async () => {
160
+ await loadTodoItems();
91
161
  callback.phase1(jsons, context);
92
162
  await syncTodoItems();
93
163
  await callback.phase2(publishJsons);
@@ -95,7 +165,7 @@ function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTo
95
165
  }));
96
166
  }
97
167
 
98
- function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJsons, syncTodoItems) {
168
+ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJsons, syncTodoItems, loadTodoItems) {
99
169
  let decoder = DcbDecode$Reventless.makeDecoder(consumedEventSchema);
100
170
  return stream => Effect.flatMap(Stream.runCollect(Stream$1.flatMap(Stream$1.mapEffect(stream, json => Effect.sync(() => {
101
171
  let envelope = Stdlib_JSON.Decode.object(json);
@@ -112,27 +182,69 @@ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJso
112
182
  return [];
113
183
  }
114
184
  })), events => Stream$1.fromIterable(events))), events => Effect.promise(async () => {
185
+ await loadTodoItems();
115
186
  callback.phase1(events);
116
187
  await syncTodoItems();
117
- await callback.phase2(publishJsons);
188
+ await callback.phase2(publishJsons, {
189
+ geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined)
190
+ });
118
191
  return await syncTodoItems();
119
192
  }));
120
193
  }
121
194
 
122
195
  function makeAutomationRegisteredHandler(entry, sliceName, callback) {
196
+ let comp = `AutomationSlice(` + sliceName + `)`;
197
+ let publishJsons = makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false));
198
+ let syncTodoItems = makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema);
199
+ let loadTodoItems = makeLoadTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema, comp);
123
200
  return {
124
- handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema))),
125
- comp: `AutomationSlice(` + sliceName + `)`
201
+ registered: {
202
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, publishJsons, syncTodoItems, loadTodoItems)),
203
+ comp: comp
204
+ },
205
+ sweep: async () => {
206
+ await loadTodoItems();
207
+ await callback.phase2(publishJsons);
208
+ return await syncTodoItems();
209
+ },
210
+ comp: comp
126
211
  };
127
212
  }
128
213
 
129
214
  function makeOutboundRegisteredHandler(entry, sliceName, consumedEventSchema, callback) {
215
+ let comp = `OutboundTranslationSlice(` + sliceName + `)`;
216
+ let publishJsons = makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false));
217
+ let syncTodoItems = makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema);
218
+ let loadTodoItems = makeLoadTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema, comp);
130
219
  return {
131
- handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema))),
132
- comp: `OutboundTranslationSlice(` + sliceName + `)`
220
+ registered: {
221
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJsons, syncTodoItems, loadTodoItems)),
222
+ comp: comp
223
+ },
224
+ sweep: async () => {
225
+ await loadTodoItems();
226
+ await callback.phase2(publishJsons, {
227
+ geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined)
228
+ });
229
+ return await syncTodoItems();
230
+ },
231
+ comp: comp
133
232
  };
134
233
  }
135
234
 
235
+ async function runSweeps(slices) {
236
+ for (let i = 0, i_finish = slices.length; i < i_finish; ++i) {
237
+ let slice = slices[i];
238
+ try {
239
+ await slice.sweep();
240
+ } catch (raw_exn) {
241
+ let exn = Primitive_exceptions.internalToException(raw_exn);
242
+ let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
243
+ Effect.runSync(EffectLogger$ReventlessCore.logError(slice.comp, undefined, `sweep failed: ` + msg));
244
+ }
245
+ }
246
+ }
247
+
136
248
  export {
137
249
  strOf,
138
250
  decodeContext,
@@ -142,9 +254,12 @@ export {
142
254
  defaultContext,
143
255
  makePublishJsons,
144
256
  makeSyncTodoItems,
257
+ makeLoadTodoItems,
258
+ capabilities,
145
259
  makeAutomationJsonEventsHandler,
146
260
  makeOutboundJsonEventsHandler,
147
261
  makeAutomationRegisteredHandler,
148
262
  makeOutboundRegisteredHandler,
263
+ runSweeps,
149
264
  }
150
265
  /* S Not a pure module */