@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.
- package/CHANGELOG.md +17 -0
- package/package.json +7 -7
- package/src/Platform.res +37 -6
- package/src/Platform.res.mjs +21 -6
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +5 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +3 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicRegistry.res +50 -0
- package/src/adapter/CommandTopic/CommandTopicRegistry.res.mjs +26 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -3
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +230 -29
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +124 -9
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +188 -3
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +107 -6
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +20 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +12 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +26 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +44 -3
- 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
|
-
|
|
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
|
-
):
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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 */
|