@reventlessdev/reventless-aws 3.0.0-alpha.188 → 3.0.0-alpha.190
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 +16 -0
- package/package.json +7 -7
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +32 -35
- package/src/adapter/Runtime/AggregateEntryPoint_Ops.res +48 -0
- package/src/adapter/Runtime/AggregateEntryPoint_Ops.res.mjs +30 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +33 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +12 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +44 -130
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +221 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +104 -0
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +33 -0
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res.mjs +26 -0
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +11 -13
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +6 -12
- package/tests/integration/AggIntegrationHarness.res +53 -0
- package/tests/integration/AggIntegrationHarness.res.mjs +92 -0
- package/tests/integration/AggTestAggregate.res +21 -0
- package/tests/integration/AggTestAggregate.res.mjs +33 -0
- package/tests/integration/AggTestAggregateBehavior.res +18 -0
- package/tests/integration/AggTestAggregateBehavior.res.mjs +35 -0
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res +103 -0
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res.mjs +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.190 (2026-07-10)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.189 (2026-07-09)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.188 (2026-07-09)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.190",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
"uuid": "^13.0.0",
|
|
11
11
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.20",
|
|
12
12
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.6",
|
|
13
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
13
14
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.25",
|
|
14
15
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.47",
|
|
15
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
16
16
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
|
|
17
|
-
"@reventlessdev/reventless-
|
|
18
|
-
"@reventlessdev/reventless-
|
|
19
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
20
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
21
|
-
"@reventlessdev/reventless-
|
|
17
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.92",
|
|
18
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.24",
|
|
19
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.14",
|
|
20
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.70",
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.150"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"rescript": "^12.3.0",
|
|
@@ -9,11 +9,13 @@ import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/Req
|
|
|
9
9
|
import { Make as eventLogOperationsMake } from "@reventlessdev/reventless-core/src/components/EventLog/EventLog_Operations.res.mjs";
|
|
10
10
|
import { Make as aggregateCallbackMake } from "@reventlessdev/reventless-core/src/components/Aggregate/Aggregate_Callback.res.mjs";
|
|
11
11
|
import { Make as commandTopicCallbackMake } from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Callback.res.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { makeCommandGenerator } from "./CommandGeneratorEntryPoint_Ops.res.mjs";
|
|
13
13
|
import { commandOutcomeToJson, runInlineAndCollect } from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
14
|
-
import { append, replay, replayStream, appendStream } from "@reventlessdev/reventless-aws/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
15
|
-
import { opsFor as pgEventLogOpsFor } from "@reventlessdev/reventless-aws/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs";
|
|
16
14
|
import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
15
|
+
// Typed cold-start core — EventLog storage-ops selection/wiring, compiler-checked
|
|
16
|
+
// against the framework signatures (see the module header and
|
|
17
|
+
// docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
18
|
+
import { makeStorageOps } from "./AggregateEntryPoint_Ops.res.mjs";
|
|
17
19
|
import { DynamoDBClient, ScanCommand } from "@aws-sdk/client-dynamodb";
|
|
18
20
|
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
19
21
|
|
|
@@ -127,24 +129,10 @@ async function checkPluginStatus(event) {
|
|
|
127
129
|
// (all aggregates share one Postgres table, unlike DynamoDB's table-per-aggregate).
|
|
128
130
|
function buildAggregateParts(specModule, behaviorModule, eventLogTableName, queueUrl, pgConnection) {
|
|
129
131
|
const patchedSpec = patchSpecId(specModule);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
append: pgOps.append,
|
|
135
|
-
replay: pgOps.replay,
|
|
136
|
-
replayStream: pgOps.replayStream,
|
|
137
|
-
appendStream: pgOps.appendStream,
|
|
138
|
-
};
|
|
139
|
-
} else {
|
|
140
|
-
const resolvedTable = { name: eventLogTableName };
|
|
141
|
-
rawStorageOps = {
|
|
142
|
-
append: append(resolvedTable),
|
|
143
|
-
replay: replay(resolvedTable),
|
|
144
|
-
replayStream: replayStream(resolvedTable),
|
|
145
|
-
appendStream: appendStream(resolvedTable),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
132
|
+
// Typed core: backend-specific EventLog storage ops (Postgres when pgConnection
|
|
133
|
+
// is present, else DynamoDB). Returns the full 6-field EventLog_Adapter.operations
|
|
134
|
+
// — the shell previously dropped latestSnapshot/writeSnapshot.
|
|
135
|
+
const rawStorageOps = makeStorageOps(eventLogTableName, pgConnection);
|
|
148
136
|
const eventLogOps = eventLogOperationsMake(patchedSpec)({
|
|
149
137
|
Spec: patchedSpec,
|
|
150
138
|
EventTopic: { Spec: patchedSpec },
|
|
@@ -178,13 +166,11 @@ function buildCommandGeneratorHandler(parts, dispatchMode) {
|
|
|
178
166
|
const publishJsonsAndWait = dispatchMode === "async"
|
|
179
167
|
? undefined
|
|
180
168
|
: (jsons) => runInlineAndCollect(jsons, handleJsonCommands);
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// primitive value" once the log line tries to print "CommandGenerator(<schema>)").
|
|
187
|
-
const generateCommand = makeGenerateCommand(publishJsons, publishJsonsAndWait, patchedSpec.name, patchedSpec.commandSchema, "Aggregate", undefined);
|
|
169
|
+
// Typed core (CommandGeneratorEntryPoint_Ops.res) pins the arg order and the
|
|
170
|
+
// componentKind/stripIdFromParams positions against the framework signature.
|
|
171
|
+
// stripIdFromParams=true matches the framework default the old shell relied on
|
|
172
|
+
// by passing `undefined`.
|
|
173
|
+
const generateCommand = makeCommandGenerator(publishJsons, publishJsonsAndWait, patchedSpec.name, patchedSpec.commandSchema, "Aggregate", true);
|
|
188
174
|
return (event, _context) => {
|
|
189
175
|
// Add identity fallback: use payload.identity if present, otherwise construct from meta.user.
|
|
190
176
|
const identity = (event.identity != null && typeof event.identity === 'object')
|
|
@@ -256,21 +242,32 @@ function extractCorrelationId(records) {
|
|
|
256
242
|
// variant (Aggregate_Builder_Single_Async) sets DISPATCH_MODE=async.
|
|
257
243
|
const DISPATCH_MODE = process.env["DISPATCH_MODE"] === "async" ? "async" : "sync";
|
|
258
244
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
245
|
+
// Exported for tests: build the [cmdTopicHandlers, cmdGenHandlers] pair from an
|
|
246
|
+
// already-parsed `HANDLER_CONFIG` shape, with an injectable module loader. In
|
|
247
|
+
// production, `buildAllHandlers` wraps this with the env config + the Lambda's
|
|
248
|
+
// `/var/task/node_modules` dynamic import; the integration test injects a loader
|
|
249
|
+
// returning pre-imported spec/behavior modules so it can drive the real
|
|
250
|
+
// cmdGenHandler end-to-end against DynamoDB Local. (Mirrors the DCB entry point.)
|
|
251
|
+
export async function buildHandlersForConfig(config, opts = {}) {
|
|
252
|
+
const loadModule = opts.loadModule ?? dynamicImport;
|
|
253
|
+
const dispatchMode = opts.dispatchMode ?? DISPATCH_MODE;
|
|
262
254
|
const cmdTopicHandlers = {};
|
|
263
255
|
const cmdGenHandlers = {};
|
|
264
|
-
await Promise.all(config.handlers.map(async h => {
|
|
265
|
-
const specModule = await
|
|
266
|
-
const behaviorModule = await
|
|
256
|
+
await Promise.all((config.handlers || []).map(async h => {
|
|
257
|
+
const specModule = await loadModule(h.specModule);
|
|
258
|
+
const behaviorModule = await loadModule(h.behaviorModule);
|
|
267
259
|
const parts = buildAggregateParts(specModule, behaviorModule, h.eventLogTable, h.queueUrl, h.pgConnection);
|
|
268
260
|
cmdTopicHandlers[h.queueArn] = parts.sqsHandler;
|
|
269
|
-
cmdGenHandlers[specModule.name] = buildCommandGeneratorHandler(parts,
|
|
261
|
+
cmdGenHandlers[specModule.name] = buildCommandGeneratorHandler(parts, dispatchMode);
|
|
270
262
|
}));
|
|
271
263
|
return [cmdTopicHandlers, cmdGenHandlers];
|
|
272
264
|
}
|
|
273
265
|
|
|
266
|
+
async function buildAllHandlers() {
|
|
267
|
+
const configStr = process.env["HANDLER_CONFIG"] || '{"handlers":[]}';
|
|
268
|
+
return buildHandlersForConfig(JSON.parse(configStr));
|
|
269
|
+
}
|
|
270
|
+
|
|
274
271
|
const initPromise = buildAllHandlers();
|
|
275
272
|
|
|
276
273
|
export async function handler(event, context) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Typed cold-start core for the Aggregate Lambda entry point.
|
|
2
|
+
//
|
|
3
|
+
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
4
|
+
// The `.mjs` shell owns the untyped seams — reading `HANDLER_CONFIG`, the dynamic
|
|
5
|
+
// `import()` of user Spec/Behavior modules, and the functor wiring that consumes
|
|
6
|
+
// those runtime-loaded modules (`EventLog_Operations.Make`, `Aggregate_Callback.Make`,
|
|
7
|
+
// `CommandTopic_Callback.Make`). This module owns the one piece that is pure
|
|
8
|
+
// typed framework wiring: selecting and building the EventLog storage operations.
|
|
9
|
+
//
|
|
10
|
+
// The `.mjs` previously built these with positional `append(resolvedTable)` /
|
|
11
|
+
// `replay(...)` calls into the compiled runtime, and — crucially — assembled only
|
|
12
|
+
// FOUR of the six `EventLog_Adapter.operations` fields, silently dropping
|
|
13
|
+
// `latestSnapshot` / `writeSnapshot` on both the DynamoDB and Postgres paths. The
|
|
14
|
+
// deploy-time adapter (EventLogStorage_DynamoDb.res) wires all six. Typing the
|
|
15
|
+
// return as `EventLog_Adapter.operations` forces parity: the Lambda now matches
|
|
16
|
+
// the deploy-time adapter exactly. Snapshot ops are a no-op when snapshotting is
|
|
17
|
+
// disabled (the default) and correct when it is enabled — where the old shell
|
|
18
|
+
// would have called `undefined`.
|
|
19
|
+
|
|
20
|
+
// The `pgConnection` object as it arrives in `HANDLER_CONFIG` — the resolved
|
|
21
|
+
// `PgConnection.connectionConfig` ({host,port,database,username,secretArn}).
|
|
22
|
+
// Present iff this aggregate is Postgres-backed; `tableName` doubles as the
|
|
23
|
+
// Postgres `event_log.log_name` (all aggregates share one Postgres table).
|
|
24
|
+
type pgConnectionJson
|
|
25
|
+
external asConnectionConfig: pgConnectionJson => PgConnection.connectionConfig = "%identity"
|
|
26
|
+
|
|
27
|
+
let makeStorageOps = (
|
|
28
|
+
~tableName: string,
|
|
29
|
+
~pgConnection: option<pgConnectionJson>,
|
|
30
|
+
): ReventlessCore.EventLog_Adapter.operations =>
|
|
31
|
+
switch pgConnection {
|
|
32
|
+
| Some(pg) => EventLogStorage_Postgres_Runtime.opsFor(pg->asConnectionConfig, ~logName=tableName)
|
|
33
|
+
| None =>
|
|
34
|
+
let table: Util_DynamoDb_Runtime.resolvedTable = {
|
|
35
|
+
id: "",
|
|
36
|
+
name: tableName,
|
|
37
|
+
arn: "",
|
|
38
|
+
hashKey: "id",
|
|
39
|
+
}
|
|
40
|
+
{
|
|
41
|
+
ReventlessCore.EventLog_Adapter.append: EventLogStorage_DynamoDb_Runtime.append(table),
|
|
42
|
+
replay: EventLogStorage_DynamoDb_Runtime.replay(table),
|
|
43
|
+
replayStream: EventLogStorage_DynamoDb_Runtime.replayStream(table),
|
|
44
|
+
appendStream: EventLogStorage_DynamoDb_Runtime.appendStream(table),
|
|
45
|
+
latestSnapshot: EventLogStorage_DynamoDb_Runtime.latestSnapshot(table),
|
|
46
|
+
writeSnapshot: EventLogStorage_DynamoDb_Runtime.writeSnapshot(table),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
|
+
import * as EventLogStorage_DynamoDb_Runtime$ReventlessAws from "../EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
5
|
+
import * as EventLogStorage_Postgres_Runtime$ReventlessAws from "../EventLog/EventLogStorage_Postgres_Runtime.res.mjs";
|
|
6
|
+
|
|
7
|
+
function makeStorageOps(tableName, pgConnection) {
|
|
8
|
+
if (pgConnection !== undefined) {
|
|
9
|
+
return EventLogStorage_Postgres_Runtime$ReventlessAws.opsFor(Primitive_option.valFromOption(pgConnection), tableName);
|
|
10
|
+
}
|
|
11
|
+
let table = {
|
|
12
|
+
id: "",
|
|
13
|
+
name: tableName,
|
|
14
|
+
arn: "",
|
|
15
|
+
hashKey: "id"
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
append: EventLogStorage_DynamoDb_Runtime$ReventlessAws.append(table),
|
|
19
|
+
replay: EventLogStorage_DynamoDb_Runtime$ReventlessAws.replay(table),
|
|
20
|
+
replayStream: EventLogStorage_DynamoDb_Runtime$ReventlessAws.replayStream(table),
|
|
21
|
+
appendStream: EventLogStorage_DynamoDb_Runtime$ReventlessAws.appendStream(table),
|
|
22
|
+
latestSnapshot: EventLogStorage_DynamoDb_Runtime$ReventlessAws.latestSnapshot(table),
|
|
23
|
+
writeSnapshot: EventLogStorage_DynamoDb_Runtime$ReventlessAws.writeSnapshot(table)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
makeStorageOps,
|
|
29
|
+
}
|
|
30
|
+
/* EventLogStorage_DynamoDb_Runtime-ReventlessAws Not a pure module */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Typed cold-start core shared by the two command-path entry-point shells
|
|
2
|
+
// (AggregateEntryPoint.mjs, DcbCommandTopicEntryPoint.mjs).
|
|
3
|
+
//
|
|
4
|
+
// Both shells build their AppSync CommandGenerator handler by calling the
|
|
5
|
+
// compiled `CommandGenerator_Callback.makeGenerateCommand` positionally — a call
|
|
6
|
+
// their comments flagged as fragile: omitting the trailing `stripIdFromParams`
|
|
7
|
+
// shifts every argument left, sending the schema object through the `serviceName`
|
|
8
|
+
// slot and throwing "Cannot convert object to primitive value" at the first log
|
|
9
|
+
// line. This wrapper pins that correspondence: the arg order, the
|
|
10
|
+
// `commandComponentKind` variant, and an explicit (non-defaulted)
|
|
11
|
+
// `stripIdFromParams` are all compiler-checked against the framework signature,
|
|
12
|
+
// so a signature change is a build error rather than a silent runtime shift.
|
|
13
|
+
//
|
|
14
|
+
// The shell still calls this positionally from JS, but the wrapper's OWN call
|
|
15
|
+
// into the framework is typed — this file is the stable contract the shells pin
|
|
16
|
+
// to, far less likely to drift than the framework internal.
|
|
17
|
+
|
|
18
|
+
let makeCommandGenerator = (
|
|
19
|
+
~publishJsons,
|
|
20
|
+
~publishJsonsAndWait: option<_>,
|
|
21
|
+
~serviceName,
|
|
22
|
+
~commandSchema,
|
|
23
|
+
~componentKind: ReventlessCore.CommandGenerator_Callback.commandComponentKind,
|
|
24
|
+
~stripIdFromParams: bool,
|
|
25
|
+
): ReventlessCore.CommandGenerator.commandGenerator =>
|
|
26
|
+
ReventlessCore.CommandGenerator_Callback.makeGenerateCommand(
|
|
27
|
+
~publishJsons,
|
|
28
|
+
~publishJsonsAndWait?,
|
|
29
|
+
~serviceName,
|
|
30
|
+
~commandSchema,
|
|
31
|
+
~componentKind,
|
|
32
|
+
~stripIdFromParams,
|
|
33
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as CommandGenerator_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
4
|
+
|
|
5
|
+
function makeCommandGenerator(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, stripIdFromParams) {
|
|
6
|
+
return CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, stripIdFromParams);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
makeCommandGenerator,
|
|
11
|
+
}
|
|
12
|
+
/* CommandGenerator_Callback-ReventlessCore Not a pure module */
|
|
@@ -7,22 +7,17 @@ import * as Chunk from "effect/Chunk";
|
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stream from "effect/Stream";
|
|
9
9
|
import { patchSpecId, makeQueueRef, log, pluginName } from "./HandlerFactoryHelpers.mjs";
|
|
10
|
-
import { decodeCommand$p as decodeCommandPrime } from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
11
10
|
import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
|
|
12
|
-
import {
|
|
13
|
-
extractVariantNames,
|
|
14
|
-
deriveEffectiveScope,
|
|
15
|
-
derivePartitionTag,
|
|
16
|
-
} from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
17
|
-
import { $$String as IdString } from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
18
11
|
import { Make as dcbEventLogOperationsMake } from "@reventlessdev/reventless-core/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
|
|
19
|
-
import {
|
|
20
|
-
import { makeGenerateCommand } from "@reventlessdev/reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
12
|
+
import { makeCommandGenerator } from "./CommandGeneratorEntryPoint_Ops.res.mjs";
|
|
21
13
|
import { commandOutcomeToJson, runInlineAndCollect } from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
22
14
|
import { json as jsonSchema } from "sury/src/S.res.mjs";
|
|
23
|
-
import { read, append, readStream } from "@reventlessdev/reventless-aws/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
24
|
-
import { opsFor as pgDcbEventLogOpsFor } from "@reventlessdev/reventless-aws/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs";
|
|
25
15
|
import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
16
|
+
// Typed cold-start core — scope/partitionTag derivation, storage-ops wiring, and
|
|
17
|
+
// per-slice handler building (functor + decode + handleCommands), all
|
|
18
|
+
// compiler-checked against the framework signatures (see the module header and
|
|
19
|
+
// docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
20
|
+
import { deriveScope, commandTypeNames, makeStorageOps, buildSliceHandler } from "./DcbCommandTopicEntryPoint_Ops.res.mjs";
|
|
26
21
|
|
|
27
22
|
const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
|
|
28
23
|
|
|
@@ -78,10 +73,6 @@ function extractCorrelationId(records) {
|
|
|
78
73
|
return undefined;
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
function getIdStringSchema() {
|
|
82
|
-
return IdString.schema;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
76
|
// Exported for tests: build the [sqsHandler, cmdGenHandler] pair from an
|
|
86
77
|
// already-parsed `HANDLER_CONFIG` shape, with an injectable module loader.
|
|
87
78
|
// In production, `buildHandler` below wraps this with the env-config + the
|
|
@@ -91,94 +82,33 @@ function getIdStringSchema() {
|
|
|
91
82
|
export async function buildHandlersForConfig(config, opts = {}) {
|
|
92
83
|
const loadModule = opts.loadModule ?? dynamicImport;
|
|
93
84
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// below must be built with them — so slices load before anything else.
|
|
85
|
+
// Untyped boundary: dynamically import + id-patch the user Spec/Behavior
|
|
86
|
+
// modules. Their types are unknowable here — this is the one seam the typed
|
|
87
|
+
// core (below) cannot own, so it stays in the shell.
|
|
98
88
|
const loadedSlices = await Promise.all(
|
|
99
89
|
config.stateChangeSliceModules.map(async ({ spec, behavior }) => {
|
|
100
90
|
const specModule = await loadModule(spec);
|
|
101
91
|
const behaviorModule = await loadModule(behavior);
|
|
102
|
-
|
|
103
|
-
return { patchedSpec, behaviorModule };
|
|
92
|
+
return { patchedSpec: patchSpecId(specModule), behaviorModule };
|
|
104
93
|
})
|
|
105
94
|
);
|
|
106
95
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
96
|
+
// Typed core: scope (crossPartitionTagKeys / tagKeysByEventType) + storage
|
|
97
|
+
// `partitionTag` derivation and backend-specific storage-ops wiring, all
|
|
98
|
+
// compiler-checked against the framework signatures — the single source of
|
|
99
|
+
// truth shared with Dcb_Builder.res. The two prod incidents this prevents are
|
|
100
|
+
// documented in DcbCommandTopicEntryPoint_Ops.res. `pgConnection`, when present
|
|
101
|
+
// in HANDLER_CONFIG, selects the Postgres backend (dcbEventLogTableName doubles
|
|
102
|
+
// as `dcb_event.log_name`); absence keeps the DynamoDB path byte-identical.
|
|
103
|
+
const specs = loadedSlices.map(({ patchedSpec }) => patchedSpec);
|
|
104
|
+
const scope = deriveScope(specs);
|
|
105
|
+
const { crossPartitionTagKeys, tagKeysByEventType } = scope;
|
|
106
|
+
const rawStorageOps = makeStorageOps(
|
|
107
|
+
config.dcbEventLogTableName,
|
|
108
|
+
config.pgConnection,
|
|
109
|
+
scope,
|
|
120
110
|
);
|
|
121
111
|
|
|
122
|
-
// Derive the storage partition tag the SAME way Dcb_Builder.res does at
|
|
123
|
-
// deploy time (over the produced event schemas). This MUST reach the DynamoDB
|
|
124
|
-
// `append` — without it `partitionTag` defaults to `None`, the composite-fence
|
|
125
|
-
// collapse (gated on `Composite`) never fires, and a `@compositePartitionTag`
|
|
126
|
-
// slice writes one fence PER MEMBER instead of one synthetic composite fence.
|
|
127
|
-
// A deploy-time fan-out sharing low-cardinality members (environment, plugin,
|
|
128
|
-
// …) then turns those member fences hot → TransactionConflict → retries
|
|
129
|
-
// exhausted. The deploy-time adapter (DcbEventLogStorage_DynamoDb.make) already
|
|
130
|
-
// threads it; the deployed Lambda dropped it. See
|
|
131
|
-
// docs/plans/dcb-composite-fence-residual-burst-contention.md.
|
|
132
|
-
// `derivePartitionTag` throws only on a misconfigured spec, which the deploy
|
|
133
|
-
// would already have rejected; guard defensively so a runtime edge case
|
|
134
|
-
// degrades to the (old) untagged behaviour rather than crashing cold start.
|
|
135
|
-
let partitionTag = undefined;
|
|
136
|
-
try {
|
|
137
|
-
partitionTag = derivePartitionTag(
|
|
138
|
-
loadedSlices.map(({ patchedSpec }) => [
|
|
139
|
-
patchedSpec.name,
|
|
140
|
-
patchedSpec.moduleUrl ?? patchedSpec.name,
|
|
141
|
-
patchedSpec.eventSchema,
|
|
142
|
-
])
|
|
143
|
-
);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
log.warn("could not derive partitionTag; falling back to untagged fences: " + (err && err.message), { comp: "DcbCommandTopicRuntime" });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// `pgConnection`, when present in HANDLER_CONFIG, selects the Postgres DCB
|
|
149
|
-
// runtime (dcbEventLogTableName doubles as the `dcb_event.log_name`); absence
|
|
150
|
-
// keeps the DynamoDB path byte-identical. NB: this swaps only the *storage*
|
|
151
|
-
// ops — event propagation (within-plugin projections + cross-plugin SNS) is
|
|
152
|
-
// stream-driven on AWS and is handled separately by the change-feed relay
|
|
153
|
-
// (see docs/plans/aws-postgres-change-feed-bridge.md), not by this Lambda.
|
|
154
|
-
const rawStorageOps = config.pgConnection
|
|
155
|
-
? (() => {
|
|
156
|
-
// C2: `lockStrategy` ("AdvisoryLocks" | "RowLocks") is the polyvariant's
|
|
157
|
-
// string name; `opsFor` defaults to "AdvisoryLocks" when it's absent.
|
|
158
|
-
const pgOps = pgDcbEventLogOpsFor(
|
|
159
|
-
config.pgConnection,
|
|
160
|
-
config.dcbEventLogTableName,
|
|
161
|
-
config.pgConnection.lockStrategy,
|
|
162
|
-
);
|
|
163
|
-
return { read: pgOps.read, append: pgOps.append, readStream: pgOps.readStream };
|
|
164
|
-
})()
|
|
165
|
-
: (() => {
|
|
166
|
-
const resolvedTable = { name: config.dcbEventLogTableName };
|
|
167
|
-
// Positional args match the ReScript-compiled signatures:
|
|
168
|
-
// read(table, crossPartitionTagKeys)
|
|
169
|
-
// append(table, partitionTag, crossPartitionTagKeys)
|
|
170
|
-
// readStream(table, crossPartitionTagKeys)
|
|
171
|
-
// Threading partitionTag activates the composite-fence collapse; threading
|
|
172
|
-
// crossPartitionTagKeys routes single-tag `@crossPartition` reads to the
|
|
173
|
-
// per-tag GSI and bumps their fences on every carrier — matching the
|
|
174
|
-
// deploy-time adapter's storage ops exactly.
|
|
175
|
-
return {
|
|
176
|
-
read: read(resolvedTable, crossPartitionTagKeys),
|
|
177
|
-
append: append(resolvedTable, partitionTag, crossPartitionTagKeys),
|
|
178
|
-
readStream: readStream(resolvedTable, crossPartitionTagKeys),
|
|
179
|
-
};
|
|
180
|
-
})();
|
|
181
|
-
|
|
182
112
|
const handlersByType = {};
|
|
183
113
|
const sharedDcbEventLogOps = dcbEventLogOperationsMake({
|
|
184
114
|
name: config.pluginName,
|
|
@@ -191,35 +121,20 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
191
121
|
});
|
|
192
122
|
|
|
193
123
|
loadedSlices.forEach(({ patchedSpec, behaviorModule }) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const jsonHandler =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
})),
|
|
208
|
-
opt => opt.TAG === "Some" ? Stream.make(opt._0) : Stream.empty
|
|
209
|
-
);
|
|
210
|
-
// Positional args match StateChangeSlice_Callback.res's compiled signature:
|
|
211
|
-
// (tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, dcbEventLog, stream).
|
|
212
|
-
return sliceCallback.handleCommands(
|
|
213
|
-
tagKeysByEventType,
|
|
214
|
-
crossPartitionTagKeys,
|
|
215
|
-
sharedDcbEventLogOps,
|
|
216
|
-
decodedStream,
|
|
217
|
-
);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
typeNames.forEach(typeName => {
|
|
124
|
+
// Typed core: builds the slice callback (functor), the JSON→command decode,
|
|
125
|
+
// and the handleCommands invocation — all compiler-checked (was the fragile
|
|
126
|
+
// positional call this file's line-146 comment warned about; see the
|
|
127
|
+
// 2026-06-21 arity-drift incident referenced in the _Ops header).
|
|
128
|
+
const jsonHandler = buildSliceHandler(
|
|
129
|
+
patchedSpec,
|
|
130
|
+
behaviorModule,
|
|
131
|
+
tagKeysByEventType,
|
|
132
|
+
crossPartitionTagKeys,
|
|
133
|
+
sharedDcbEventLogOps,
|
|
134
|
+
);
|
|
135
|
+
for (const typeName of commandTypeNames(patchedSpec)) {
|
|
221
136
|
handlersByType[typeName] = jsonHandler;
|
|
222
|
-
}
|
|
137
|
+
}
|
|
223
138
|
});
|
|
224
139
|
|
|
225
140
|
// Composite handler used by Route 2 (SQS event source) AND inline dispatch
|
|
@@ -251,20 +166,19 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
251
166
|
|
|
252
167
|
// Sync (default): inline-dispatch the command via the same composite handler
|
|
253
168
|
// that Route 2 uses, so the AppSync resolver gets a typed Accepted/Rejected
|
|
254
|
-
// outcome. Async: undefined →
|
|
169
|
+
// outcome. Async: undefined → makeCommandGenerator falls back to publishJsons
|
|
255
170
|
// and returns Pending. The schema is permissive (S.json) because AppSync has
|
|
256
171
|
// already validated input against the SDL — per-slice schemas reapply inside
|
|
257
|
-
// the
|
|
172
|
+
// the typed core's buildSliceHandler decode.
|
|
258
173
|
const publishJsonsAndWait = DISPATCH_MODE === "async"
|
|
259
174
|
? undefined
|
|
260
175
|
: (jsons) => runInlineAndCollect(jsons, compositeJsonCommandsHandler);
|
|
261
176
|
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
const generateCommand = makeGenerateCommand(
|
|
177
|
+
// Typed core (CommandGeneratorEntryPoint_Ops.res) pins the arg order against
|
|
178
|
+
// the framework signature. stripIdFromParams=false: DCB slices may declare a
|
|
179
|
+
// literal `id` field as part of the command schema (composite partition keys
|
|
180
|
+
// etc.) — don't strip it.
|
|
181
|
+
const generateCommand = makeCommandGenerator(
|
|
268
182
|
publishJsons,
|
|
269
183
|
publishJsonsAndWait,
|
|
270
184
|
config.pluginName,
|