@reventlessdev/reventless-aws 3.0.0-alpha.267 → 3.0.0-alpha.269
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 +15 -0
- package/package.json +9 -9
- package/src/adapter/EventLog/EventLogStorage.res +7 -1
- package/src/adapter/Runtime/StateTopicPublish.mjs +51 -8
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +66 -12
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +29 -6
- package/src/util/Util_StoreLayout.res +1 -1
- package/src/util/Util_StoreLayout.res.mjs +3 -4
- package/tests/StateChangeDescriptorParityTest.res +58 -0
- package/tests/StateChangeDescriptorParityTest.res.mjs +60 -0
- package/tests/StateTopicPublishTest.res +29 -5
- package/tests/StateTopicPublishTest.res.mjs +32 -5
- package/tests/stateChangeDescriptorParity.mjs +102 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDb.res +0 -27
- package/src/adapter/EventLog/EventLogStorage_DynamoDb.res.mjs +0 -36
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.269 (2026-08-07)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core,aws:** add the EventLogProvisioning seam for out-of-tree consumers ([82477b3](https://github.com/ReventlessDev/reventless-core/commit/82477b38c1ef90fbe3a6f4a114a9798770329470))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.268 (2026-08-05)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **local,aws:** carry the changed row in the live-update descriptor ([9272e2e](https://github.com/ReventlessDev/reventless-core/commit/9272e2e18e9e1d740f9a2c5aa83eccbcb41feff7))
|
|
18
|
+
* **local:** scoped seed:reset for the local platform ([a4dd003](https://github.com/ReventlessDev/reventless-core/commit/a4dd0033b7d01be23be91bac3f01d38ebeab7d45))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.267 (2026-08-04)
|
|
7
22
|
|
|
8
23
|
### Features
|
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.269",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
12
12
|
"sury": "11.0.0-alpha.4",
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
14
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.4",
|
|
15
15
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
16
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.
|
|
18
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
17
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.66",
|
|
18
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.2",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
20
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.126",
|
|
21
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.212",
|
|
22
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
23
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.76",
|
|
24
20
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
25
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.214",
|
|
22
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
23
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.78",
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.101",
|
|
25
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.127"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"rescript": "12.3.0",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
// Classic event-log tables stream unconditionally. A stream costs nothing until it
|
|
2
|
+
// is consumed, and without one the `EventLogProvisioning` seam has nothing to hand
|
|
3
|
+
// a backend for a classic log while DCB logs (which always stream) work — a silent
|
|
4
|
+
// capability difference between two adapters presenting the same port. The
|
|
5
|
+
// non-streaming maker had no callers; `DynamoDb` stays as an alias so out-of-tree
|
|
6
|
+
// references keep compiling.
|
|
7
|
+
module DynamoDb = EventLogStorage_DynamoDbStream
|
|
2
8
|
module DynamoDbStream = EventLogStorage_DynamoDbStream
|
|
3
9
|
module DynamoDb_Runtime = EventLogStorage_DynamoDb_Runtime
|
|
4
10
|
module Postgres = EventLogStorage_Postgres
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Channel: /default/{pathSegment(topicName)}/{pathSegment(entityKey)}
|
|
14
14
|
// Descriptor: { changeKind: "Updated" | "Removed", id: <entityKey>,
|
|
15
|
-
// sortKeyValue?: <updatedAt | createdAt if present
|
|
15
|
+
// sortKeyValue?: <updatedAt | createdAt if present>,
|
|
16
|
+
// seq: <monotonic ordering token>,
|
|
17
|
+
// state?: <the full new row, saves only> }
|
|
16
18
|
//
|
|
17
19
|
// changeKind is fixed per operation (save→"Updated", delete→"Removed"): a
|
|
18
20
|
// Postgres upsert doesn't distinguish insert vs update, and the Sqlite/InMemory
|
|
@@ -64,8 +66,8 @@ function pathSegment(value) {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// Pick the natural sort timestamp from the saved state (updatedAt preferred,
|
|
67
|
-
// createdAt fallback). Mirrors
|
|
68
|
-
//
|
|
69
|
+
// createdAt fallback). Mirrors StateTopic_AppSync_Ops.pickSortKeyValue and
|
|
70
|
+
// LocalStateChangeDescriptor.pickSortKeyValue.
|
|
69
71
|
function pickSortKeyValue(state) {
|
|
70
72
|
if (state && typeof state === "object" && !Array.isArray(state)) {
|
|
71
73
|
if (typeof state.updatedAt === "string") return state.updatedAt;
|
|
@@ -74,21 +76,62 @@ function pickSortKeyValue(state) {
|
|
|
74
76
|
return undefined;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
// Cap on the serialised state payload, in characters. Must match
|
|
80
|
+
// LocalStateChangeDescriptor.maxStateChars and StateTopic_AppSync_Ops.maxStateChars
|
|
81
|
+
// — see the local module for the reasoning.
|
|
82
|
+
const MAX_STATE_CHARS = 60 * 1024;
|
|
83
|
+
|
|
84
|
+
// Monotonic ordering token, seeded from the wall clock so it keeps rising across
|
|
85
|
+
// container restarts. Per Lambda instance: two instances writing the same entity
|
|
86
|
+
// within one millisecond have no defined order. This is an ordering hint, not a
|
|
87
|
+
// lock, and it is sparse — the client rule is "greater than what I hold", never
|
|
88
|
+
// "exactly one more". See docs/analysis/live-update-descriptor-sequencing.md.
|
|
89
|
+
let lastSequence = 0;
|
|
90
|
+
export function nextSequence() {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
lastSequence = now > lastSequence ? now : lastSequence + 1;
|
|
93
|
+
return String(lastSequence);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Assemble the descriptor. `state` is the full new row for a save, undefined for a
|
|
97
|
+
// delete — which carries neither `state` nor `sortKeyValue`, matching the other two
|
|
98
|
+
// implementations. Over the size cap the state is dropped and the downgrade logged:
|
|
99
|
+
// a metadata-only descriptor still tells the client to refetch, where a publish
|
|
100
|
+
// rejected for size would tell it nothing.
|
|
101
|
+
export function makeDescriptor({ changeKind, entityKey, state, seq }) {
|
|
102
|
+
const descriptor = { changeKind, id: entityKey };
|
|
103
|
+
if (state !== undefined) {
|
|
104
|
+
const sortKeyValue = pickSortKeyValue(state);
|
|
105
|
+
if (sortKeyValue !== undefined) descriptor.sortKeyValue = sortKeyValue;
|
|
106
|
+
}
|
|
107
|
+
descriptor.seq = seq;
|
|
108
|
+
if (state !== undefined) {
|
|
109
|
+
const encoded = JSON.stringify(state);
|
|
110
|
+
if (encoded.length <= MAX_STATE_CHARS) {
|
|
111
|
+
descriptor.state = state;
|
|
112
|
+
} else {
|
|
113
|
+
console.warn(
|
|
114
|
+
"STATE_PAYLOAD_DOWNGRADED id=" + entityKey + " chars=" + encoded.length,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return descriptor;
|
|
119
|
+
}
|
|
120
|
+
|
|
77
121
|
// Build the descriptor + channel and POST to {endpoint}/event.
|
|
78
122
|
// - endpoint: base AppSync Events HTTP endpoint (no trailing /event).
|
|
79
123
|
// - region: AWS region for the SigV4 credential scope.
|
|
80
124
|
// - topicName: the plugin-prefixed query LIST field name (channel root).
|
|
81
125
|
// - entityKey: ORIGINAL entity key (id or `id-subKey`) — descriptor body + path.
|
|
82
126
|
// - changeKind: "Updated" (save) | "Removed" (delete).
|
|
83
|
-
// -
|
|
127
|
+
// - state: the full new row for a save; omitted for a delete.
|
|
84
128
|
// - dedupeId: AppSync publish `id` (idempotency hint).
|
|
85
|
-
export async function publishStateChange({ endpoint, region, topicName, entityKey, changeKind,
|
|
129
|
+
export async function publishStateChange({ endpoint, region, topicName, entityKey, changeKind, state, dedupeId }) {
|
|
86
130
|
if (!endpoint || !topicName) return; // not live-enabled — no-op
|
|
87
131
|
try {
|
|
88
132
|
const url = new URL(endpoint);
|
|
89
133
|
const channel = "/default/" + pathSegment(topicName) + "/" + pathSegment(entityKey);
|
|
90
|
-
const descriptor = { changeKind,
|
|
91
|
-
if (sortKeyValue !== undefined) descriptor.sortKeyValue = sortKeyValue;
|
|
134
|
+
const descriptor = makeDescriptor({ changeKind, entityKey, state, seq: nextSequence() });
|
|
92
135
|
const body = JSON.stringify({
|
|
93
136
|
id: dedupeId || (topicName + ":" + entityKey + ":" + changeKind),
|
|
94
137
|
channel,
|
|
@@ -154,7 +197,7 @@ export function withLiveUpdates(ops, liveConfig) {
|
|
|
154
197
|
endpoint, region, topicName,
|
|
155
198
|
entityKey: entityKeyFor(id, state, subIdField),
|
|
156
199
|
changeKind: "Updated",
|
|
157
|
-
|
|
200
|
+
state,
|
|
158
201
|
});
|
|
159
202
|
|
|
160
203
|
const publishRemovedKey = async (entityKey) =>
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Triggered by DynamoDB streams (one shared Lambda; routing is per-record via
|
|
8
8
|
// STATE_TOPIC_MAP). For each changed row it derives the entity channel and a
|
|
9
|
-
// `{changeKind, id, sortKeyValue?}` descriptor and publishes it. 4xx
|
|
10
|
-
// are logged and skipped; 5xx / network failures are recorded and
|
|
11
|
-
// the batch so the EventSourceMapping retries
|
|
9
|
+
// `{changeKind, id, sortKeyValue?, seq, state?}` descriptor and publishes it. 4xx
|
|
10
|
+
// failures are logged and skipped; 5xx / network failures are recorded and
|
|
11
|
+
// rethrown after the batch so the EventSourceMapping retries
|
|
12
|
+
// (bisectBatchOnFunctionError).
|
|
13
|
+
//
|
|
14
|
+
// The descriptor is one of three implementations of a shared wire format (the
|
|
15
|
+
// others: `LocalStateChangeDescriptor` in reventless-local, `StateTopicPublish.mjs`
|
|
16
|
+
// for Postgres read models). They share no code — this module stays Pulumi-free so
|
|
17
|
+
// a core import can't drag deploy-time code into the Lambda's import graph — so
|
|
18
|
+
// `StateChangeDescriptorParityTest` drives all three and asserts they agree.
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
let endpoint = NodeProcess.env->Dict.get("APPSYNC_ENDPOINT")->Option.getOr("")
|
|
@@ -31,6 +38,10 @@ type streamRecord = {
|
|
|
31
38
|
@as("Keys") keys: dict<attributeValue>,
|
|
32
39
|
@as("NewImage") newImage?: dict<attributeValue>,
|
|
33
40
|
@as("OldImage") oldImage?: dict<attributeValue>,
|
|
41
|
+
// Monotonic within the entity's shard lineage — records for one partition key
|
|
42
|
+
// always land on the same shard — so it orders an entity's changes without
|
|
43
|
+
// anything being written to the table. Sparse, so it detects staleness, not gaps.
|
|
44
|
+
@as("SequenceNumber") sequenceNumber?: string,
|
|
34
45
|
}
|
|
35
46
|
type record = {
|
|
36
47
|
eventID: string,
|
|
@@ -103,6 +114,52 @@ let pickSortKeyValue = (image: dict<JSON.t>): option<string> =>
|
|
|
103
114
|
| None => image->Dict.get("createdAt")->Option.flatMap(JSON.Decode.string)
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
/** Cap on the serialised state payload, in characters. Must match
|
|
118
|
+
`LocalStateChangeDescriptor.maxStateChars` and `StateTopicPublish.mjs`'s
|
|
119
|
+
MAX_STATE_CHARS — see the local module for the reasoning. */
|
|
120
|
+
let maxStateChars = 60 * 1024
|
|
121
|
+
|
|
122
|
+
/** Build the change descriptor. Split out of `processRecord` so the wire format
|
|
123
|
+
can be asserted against the other two implementations without a network call.
|
|
124
|
+
|
|
125
|
+
`image` is the unmarshalled NewImage for a save, the OldImage for a REMOVE.
|
|
126
|
+
A REMOVE carries neither `state` (there is no new row) nor `sortKeyValue`
|
|
127
|
+
(a sort position for a deleted row has no consumer, and the other two
|
|
128
|
+
implementations cannot produce one). */
|
|
129
|
+
let makeDescriptor = (
|
|
130
|
+
~changeKind: string,
|
|
131
|
+
~entityKey: string,
|
|
132
|
+
~image: dict<JSON.t>,
|
|
133
|
+
~seq: option<string>,
|
|
134
|
+
): JSON.t => {
|
|
135
|
+
let removed = changeKind == "Removed"
|
|
136
|
+
let descriptor = Dict.make()
|
|
137
|
+
descriptor->Dict.set("changeKind", JSON.Encode.string(changeKind))
|
|
138
|
+
descriptor->Dict.set("id", JSON.Encode.string(entityKey))
|
|
139
|
+
if !removed {
|
|
140
|
+
pickSortKeyValue(image)->Option.forEach(v =>
|
|
141
|
+
descriptor->Dict.set("sortKeyValue", JSON.Encode.string(v))
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
seq->Option.forEach(s => descriptor->Dict.set("seq", JSON.Encode.string(s)))
|
|
145
|
+
if !removed {
|
|
146
|
+
let state = image->JSON.Encode.object
|
|
147
|
+
let encoded = state->JSON.stringify
|
|
148
|
+
if encoded->String.length <= maxStateChars {
|
|
149
|
+
descriptor->Dict.set("state", state)
|
|
150
|
+
} else {
|
|
151
|
+
// Metadata-only still tells the client to refetch; a publish rejected for
|
|
152
|
+
// size would tell it nothing at all.
|
|
153
|
+
Console.warn(
|
|
154
|
+
`STATE_PAYLOAD_DOWNGRADED id=${entityKey} chars=${encoded
|
|
155
|
+
->String.length
|
|
156
|
+
->Int.toString}`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
descriptor->JSON.Encode.object
|
|
161
|
+
}
|
|
162
|
+
|
|
106
163
|
// Publish one record; returns Some(errorMessage) on a transient (5xx / network)
|
|
107
164
|
// failure so the caller can rethrow after the batch, None otherwise.
|
|
108
165
|
let processRecord = async (
|
|
@@ -129,20 +186,17 @@ let processRecord = async (
|
|
|
129
186
|
let entityKey = entityKeyFromRecord(dynamodb)
|
|
130
187
|
let channel = `/default/${topicRoot}/${AppSyncEventsSigner_Ops.pathSegment(entityKey)}`
|
|
131
188
|
let unmarshalled: dict<JSON.t> = AwsSdk.DynamoDb_Util_Helpers.unmarshallDict(image)
|
|
132
|
-
let descriptor =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
let descriptor = makeDescriptor(
|
|
190
|
+
~changeKind=changeKindFor(record.eventName),
|
|
191
|
+
~entityKey,
|
|
192
|
+
~image=unmarshalled,
|
|
193
|
+
~seq=dynamodb.sequenceNumber,
|
|
137
194
|
)
|
|
138
195
|
let body =
|
|
139
196
|
Dict.fromArray([
|
|
140
197
|
("id", JSON.Encode.string(record.eventID)),
|
|
141
198
|
("channel", JSON.Encode.string(channel)),
|
|
142
|
-
(
|
|
143
|
-
"events",
|
|
144
|
-
JSON.Encode.array([JSON.Encode.string(descriptor->JSON.Encode.object->JSON.stringify)]),
|
|
145
|
-
),
|
|
199
|
+
("events", JSON.Encode.array([JSON.Encode.string(descriptor->JSON.stringify)])),
|
|
146
200
|
])
|
|
147
201
|
->JSON.Encode.object
|
|
148
202
|
->JSON.stringify
|
|
@@ -89,6 +89,30 @@ function pickSortKeyValue(image) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function makeDescriptor(changeKind, entityKey, image, seq) {
|
|
93
|
+
let removed = changeKind === "Removed";
|
|
94
|
+
let descriptor = {};
|
|
95
|
+
descriptor["changeKind"] = changeKind;
|
|
96
|
+
descriptor["id"] = entityKey;
|
|
97
|
+
if (!removed) {
|
|
98
|
+
Stdlib_Option.forEach(pickSortKeyValue(image), v => {
|
|
99
|
+
descriptor["sortKeyValue"] = v;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
Stdlib_Option.forEach(seq, s => {
|
|
103
|
+
descriptor["seq"] = s;
|
|
104
|
+
});
|
|
105
|
+
if (!removed) {
|
|
106
|
+
let encoded = JSON.stringify(image);
|
|
107
|
+
if (encoded.length <= 61440) {
|
|
108
|
+
descriptor["state"] = image;
|
|
109
|
+
} else {
|
|
110
|
+
console.warn(`STATE_PAYLOAD_DOWNGRADED id=` + entityKey + ` chars=` + encoded.length.toString());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return descriptor;
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
async function processRecord(record, region, creds) {
|
|
93
117
|
let topicRoot = topicRootFromEventSourceArn(record.eventSourceARN);
|
|
94
118
|
if (topicRoot !== undefined) {
|
|
@@ -103,12 +127,7 @@ async function processRecord(record, region, creds) {
|
|
|
103
127
|
let entityKey = entityKeyFromRecord(dynamodb);
|
|
104
128
|
let channel = `/default/` + topicRoot + `/` + AppSyncEventsSigner_Ops$ReventlessAws.pathSegment(entityKey);
|
|
105
129
|
let unmarshalled = DynamoDb_Util_Helpers$AwsSdk.unmarshallDict(undefined, image);
|
|
106
|
-
let descriptor =
|
|
107
|
-
descriptor["changeKind"] = changeKindFor(record.eventName);
|
|
108
|
-
descriptor["id"] = entityKey;
|
|
109
|
-
Stdlib_Option.forEach(pickSortKeyValue(unmarshalled), v => {
|
|
110
|
-
descriptor["sortKeyValue"] = v;
|
|
111
|
-
});
|
|
130
|
+
let descriptor = makeDescriptor(changeKindFor(record.eventName), entityKey, unmarshalled, dynamodb.SequenceNumber);
|
|
112
131
|
let body = JSON.stringify(Object.fromEntries([
|
|
113
132
|
[
|
|
114
133
|
"id",
|
|
@@ -169,6 +188,8 @@ async function handler(event) {
|
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
190
|
|
|
191
|
+
let maxStateChars = 61440;
|
|
192
|
+
|
|
172
193
|
export {
|
|
173
194
|
endpoint,
|
|
174
195
|
topicMap,
|
|
@@ -177,6 +198,8 @@ export {
|
|
|
177
198
|
entityKeyFromRecord,
|
|
178
199
|
changeKindFor,
|
|
179
200
|
pickSortKeyValue,
|
|
201
|
+
maxStateChars,
|
|
202
|
+
makeDescriptor,
|
|
180
203
|
processRecord,
|
|
181
204
|
handler,
|
|
182
205
|
}
|
|
@@ -138,7 +138,7 @@ machinery: a stack that predates a change to this function empties its stores
|
|
|
138
138
|
(`seed:reset`, which wipes per plugin) and re-seeds. Carrying a permanent prefix
|
|
139
139
|
set to spare a disposable stack one wipe is the worse trade.
|
|
140
140
|
*/
|
|
141
|
-
let keyPrefixFor =
|
|
141
|
+
let keyPrefixFor = ReventlessCore.StoreLayout.keyPrefixFor
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
144
|
Who serves the declared stores — and the answer is never "both".
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as StoreLayout$ReventlessCore from "@reventlessdev/reventless-core/src/util/StoreLayout.res.mjs";
|
|
6
7
|
|
|
7
8
|
let defaultEphemeralPrefixes = ["pr-"];
|
|
8
9
|
|
|
@@ -31,10 +32,6 @@ function bucketNameFor(layout, stack, plugin, store) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function keyPrefixFor(plugin, store) {
|
|
35
|
-
return plugin + `/` + store;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
35
|
function servingFor(hasHostUiBundle, declaredBucketCount) {
|
|
39
36
|
if (declaredBucketCount !== 0) {
|
|
40
37
|
if (hasHostUiBundle) {
|
|
@@ -81,6 +78,8 @@ function pendingExpiryFor(config, store) {
|
|
|
81
78
|
})[0];
|
|
82
79
|
}
|
|
83
80
|
|
|
81
|
+
let keyPrefixFor = StoreLayout$ReventlessCore.keyPrefixFor;
|
|
82
|
+
|
|
84
83
|
export {
|
|
85
84
|
defaultEphemeralPrefixes,
|
|
86
85
|
layoutFor,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Parity of the live-update change descriptor across its three implementations:
|
|
2
|
+
//
|
|
3
|
+
// - LocalStateChangeDescriptor (reventless-local, both backends)
|
|
4
|
+
// - StateTopic_AppSync_Ops (DynamoDB stream relay)
|
|
5
|
+
// - StateTopicPublish.mjs (Postgres projection-side publisher)
|
|
6
|
+
//
|
|
7
|
+
// They share no code on purpose — the relay module stays Pulumi-free so a core
|
|
8
|
+
// import can't pull deploy-time code into its Lambda graph — which makes drift
|
|
9
|
+
// the standing risk: three sites, one wire format, and a browser that cannot tell
|
|
10
|
+
// which one produced a frame. This test is the guard.
|
|
11
|
+
//
|
|
12
|
+
// `seq` is normalised away by the harness because the three take it from different
|
|
13
|
+
// monotonic sources by design (a DynamoDB stream SequenceNumber vs a wall-clock
|
|
14
|
+
// counter); the harness still asserts each produced a non-empty string.
|
|
15
|
+
|
|
16
|
+
open JestGlobals
|
|
17
|
+
|
|
18
|
+
type case = {
|
|
19
|
+
name: string,
|
|
20
|
+
local: JSON.t,
|
|
21
|
+
relay: JSON.t,
|
|
22
|
+
postgres: JSON.t,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@module("./stateChangeDescriptorParity.mjs")
|
|
26
|
+
external buildAll: unit => promise<array<case>> = "buildAll"
|
|
27
|
+
|
|
28
|
+
describe("state-change descriptor parity", () => {
|
|
29
|
+
testAsync("every implementation builds the same descriptor", async () => {
|
|
30
|
+
let cases = await buildAll()
|
|
31
|
+
// A silent zero-case run would pass while asserting nothing.
|
|
32
|
+
expect(cases->Array.length)->toBe(6)
|
|
33
|
+
cases->Array.forEach(c => {
|
|
34
|
+
expect((c.name, c.relay))->toEqual((c.name, c.local))
|
|
35
|
+
expect((c.name, c.postgres))->toEqual((c.name, c.local))
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
testAsync("a save carries the row, a delete carries none", async () => {
|
|
40
|
+
let cases = await buildAll()
|
|
41
|
+
let stateOf = (d: JSON.t) =>
|
|
42
|
+
d->JSON.Decode.object->Option.flatMap(o => o->Dict.get("state"))
|
|
43
|
+
let byName = name => cases->Array.find(c => c.name == name)
|
|
44
|
+
|
|
45
|
+
switch byName("single-key save with updatedAt") {
|
|
46
|
+
| Some(c) => expect(stateOf(c.local)->Option.isSome)->toBe(true)
|
|
47
|
+
| None => expect("save case missing")->toBe("present")
|
|
48
|
+
}
|
|
49
|
+
switch byName("delete carries no row") {
|
|
50
|
+
| Some(c) => expect(stateOf(c.local))->toEqual(None)
|
|
51
|
+
| None => expect("delete case missing")->toBe("present")
|
|
52
|
+
}
|
|
53
|
+
switch byName("oversized row degrades to metadata only") {
|
|
54
|
+
| Some(c) => expect(stateOf(c.local))->toEqual(None)
|
|
55
|
+
| None => expect("oversized case missing")->toBe("present")
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as StateChangeDescriptorParityMjs from "./stateChangeDescriptorParity.mjs";
|
|
6
|
+
|
|
7
|
+
function buildAll(prim) {
|
|
8
|
+
return StateChangeDescriptorParityMjs.buildAll();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
globalThis.describe("state-change descriptor parity", () => {
|
|
12
|
+
globalThis.test("every implementation builds the same descriptor", async () => {
|
|
13
|
+
let cases = await StateChangeDescriptorParityMjs.buildAll();
|
|
14
|
+
globalThis.expect(cases.length).toBe(6);
|
|
15
|
+
cases.forEach(c => {
|
|
16
|
+
globalThis.expect([
|
|
17
|
+
c.name,
|
|
18
|
+
c.relay
|
|
19
|
+
]).toEqual([
|
|
20
|
+
c.name,
|
|
21
|
+
c.local
|
|
22
|
+
]);
|
|
23
|
+
globalThis.expect([
|
|
24
|
+
c.name,
|
|
25
|
+
c.postgres
|
|
26
|
+
]).toEqual([
|
|
27
|
+
c.name,
|
|
28
|
+
c.local
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
globalThis.test("a save carries the row, a delete carries none", async () => {
|
|
33
|
+
let cases = await StateChangeDescriptorParityMjs.buildAll();
|
|
34
|
+
let stateOf = d => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(d), o => o["state"]);
|
|
35
|
+
let byName = name => cases.find(c => c.name === name);
|
|
36
|
+
let c = byName("single-key save with updatedAt");
|
|
37
|
+
if (c !== undefined) {
|
|
38
|
+
globalThis.expect(Stdlib_Option.isSome(stateOf(c.local))).toBe(true);
|
|
39
|
+
} else {
|
|
40
|
+
globalThis.expect("save case missing").toBe("present");
|
|
41
|
+
}
|
|
42
|
+
let c$1 = byName("delete carries no row");
|
|
43
|
+
if (c$1 !== undefined) {
|
|
44
|
+
globalThis.expect(stateOf(c$1.local)).toEqual(undefined);
|
|
45
|
+
} else {
|
|
46
|
+
globalThis.expect("delete case missing").toBe("present");
|
|
47
|
+
}
|
|
48
|
+
let c$2 = byName("oversized row degrades to metadata only");
|
|
49
|
+
if (c$2 !== undefined) {
|
|
50
|
+
globalThis.expect(stateOf(c$2.local)).toEqual(undefined);
|
|
51
|
+
} else {
|
|
52
|
+
globalThis.expect("oversized case missing").toBe("present");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
buildAll,
|
|
59
|
+
}
|
|
60
|
+
/* Not a pure module */
|
|
@@ -17,9 +17,14 @@ open JestGlobals
|
|
|
17
17
|
let run: string => promise<JSON.t> = %raw(`
|
|
18
18
|
async function(scenario) {
|
|
19
19
|
const mod = await import("../src/adapter/Runtime/StateTopicPublish.mjs");
|
|
20
|
-
const { withLiveUpdates } = mod;
|
|
20
|
+
const { withLiveUpdates, makeDescriptor } = mod;
|
|
21
21
|
const captured = [];
|
|
22
|
-
|
|
22
|
+
// Record the descriptor the real publisher would build alongside the call, with
|
|
23
|
+
// a fixed seq so assertions stay deterministic. sortKeyValue/state derivation
|
|
24
|
+
// lives in makeDescriptor, so this keeps it under test at the wrapper level.
|
|
25
|
+
const publish = async (call) => {
|
|
26
|
+
captured.push({ ...call, descriptor: makeDescriptor({ ...call, seq: "1" }) });
|
|
27
|
+
};
|
|
23
28
|
const ok = async () => ({ TAG: "Ok", _0: undefined });
|
|
24
29
|
|
|
25
30
|
// Mock ops. load returns the composite partition's rows for the enumerate case.
|
|
@@ -53,6 +58,9 @@ async function(scenario) {
|
|
|
53
58
|
case "save-no-sortkey":
|
|
54
59
|
await w(undefined).save("p1", { id: "p1" }, "Insert", undefined);
|
|
55
60
|
break;
|
|
61
|
+
case "save-oversized":
|
|
62
|
+
await w(undefined).save("p1", { id: "p1", blob: "x".repeat(70 * 1024) }, "Insert", undefined);
|
|
63
|
+
break;
|
|
56
64
|
case "saveBatch":
|
|
57
65
|
await w(undefined).saveBatch([
|
|
58
66
|
["a", { id: "a", updatedAt: "t1" }, undefined],
|
|
@@ -86,6 +94,11 @@ let getField = (call: JSON.t, key: string): option<JSON.t> =>
|
|
|
86
94
|
|
|
87
95
|
let str = (call, key) => getField(call, key)->Option.flatMap(JSON.Decode.string)
|
|
88
96
|
|
|
97
|
+
let descriptorField = (call: JSON.t, key: string): option<JSON.t> =>
|
|
98
|
+
getField(call, "descriptor")->Option.flatMap(d => getField(d, key))
|
|
99
|
+
|
|
100
|
+
let descriptorStr = (call, key) => descriptorField(call, key)->Option.flatMap(JSON.Decode.string)
|
|
101
|
+
|
|
89
102
|
let calls = (j: JSON.t): array<JSON.t> => j->JSON.Decode.array->Option.getOr([])
|
|
90
103
|
|
|
91
104
|
describe("withLiveUpdates (B3.3a Postgres live updates)", () => {
|
|
@@ -96,8 +109,11 @@ describe("withLiveUpdates (B3.3a Postgres live updates)", () => {
|
|
|
96
109
|
let call = arr->Array.getUnsafe(0)
|
|
97
110
|
expect(str(call, "entityKey"))->toEqual(Some("p1"))
|
|
98
111
|
expect(str(call, "changeKind"))->toEqual(Some("Updated"))
|
|
99
|
-
expect(
|
|
112
|
+
expect(descriptorStr(call, "sortKeyValue"))->toEqual(Some("2024-01-02"))
|
|
100
113
|
expect(str(call, "topicName"))->toEqual(Some("Shop_Orders"))
|
|
114
|
+
// The saved row rides along so a subscriber can apply it without refetching.
|
|
115
|
+
expect(descriptorField(call, "state"))->toEqual(getField(call, "state"))
|
|
116
|
+
expect(descriptorStr(call, "seq"))->toEqual(Some("1"))
|
|
101
117
|
})
|
|
102
118
|
|
|
103
119
|
testAsync("save on a composite table uses id-subKey and createdAt fallback", async () => {
|
|
@@ -105,13 +121,21 @@ describe("withLiveUpdates (B3.3a Postgres live updates)", () => {
|
|
|
105
121
|
let call = calls(c)->Array.getUnsafe(0)
|
|
106
122
|
expect(str(call, "entityKey"))->toEqual(Some("o1-L2"))
|
|
107
123
|
expect(str(call, "changeKind"))->toEqual(Some("Updated"))
|
|
108
|
-
expect(
|
|
124
|
+
expect(descriptorStr(call, "sortKeyValue"))->toEqual(Some("2024-03-04"))
|
|
109
125
|
})
|
|
110
126
|
|
|
111
127
|
testAsync("save without updatedAt/createdAt omits sortKeyValue", async () => {
|
|
112
128
|
let c = await run("save-no-sortkey")
|
|
113
129
|
let call = calls(c)->Array.getUnsafe(0)
|
|
114
|
-
expect(
|
|
130
|
+
expect(descriptorField(call, "sortKeyValue"))->toEqual(None)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
testAsync("an oversized row degrades to a metadata-only descriptor", async () => {
|
|
134
|
+
let c = await run("save-oversized")
|
|
135
|
+
let call = calls(c)->Array.getUnsafe(0)
|
|
136
|
+
expect(descriptorField(call, "state"))->toEqual(None)
|
|
137
|
+
expect(descriptorStr(call, "changeKind"))->toEqual(Some("Updated"))
|
|
138
|
+
expect(descriptorStr(call, "seq"))->toEqual(Some("1"))
|
|
115
139
|
})
|
|
116
140
|
|
|
117
141
|
testAsync("saveBatch publishes one Updated per item", async () => {
|
|
@@ -5,9 +5,14 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
|
|
6
6
|
let run = (async function(scenario) {
|
|
7
7
|
const mod = await import("../src/adapter/Runtime/StateTopicPublish.mjs");
|
|
8
|
-
const { withLiveUpdates } = mod;
|
|
8
|
+
const { withLiveUpdates, makeDescriptor } = mod;
|
|
9
9
|
const captured = [];
|
|
10
|
-
|
|
10
|
+
// Record the descriptor the real publisher would build alongside the call, with
|
|
11
|
+
// a fixed seq so assertions stay deterministic. sortKeyValue/state derivation
|
|
12
|
+
// lives in makeDescriptor, so this keeps it under test at the wrapper level.
|
|
13
|
+
const publish = async (call) => {
|
|
14
|
+
captured.push({ ...call, descriptor: makeDescriptor({ ...call, seq: "1" }) });
|
|
15
|
+
};
|
|
11
16
|
const ok = async () => ({ TAG: "Ok", _0: undefined });
|
|
12
17
|
|
|
13
18
|
// Mock ops. load returns the composite partition's rows for the enumerate case.
|
|
@@ -41,6 +46,9 @@ let run = (async function(scenario) {
|
|
|
41
46
|
case "save-no-sortkey":
|
|
42
47
|
await w(undefined).save("p1", { id: "p1" }, "Insert", undefined);
|
|
43
48
|
break;
|
|
49
|
+
case "save-oversized":
|
|
50
|
+
await w(undefined).save("p1", { id: "p1", blob: "x".repeat(70 * 1024) }, "Insert", undefined);
|
|
51
|
+
break;
|
|
44
52
|
case "saveBatch":
|
|
45
53
|
await w(undefined).saveBatch([
|
|
46
54
|
["a", { id: "a", updatedAt: "t1" }, undefined],
|
|
@@ -76,6 +84,14 @@ function str(call, key) {
|
|
|
76
84
|
return Stdlib_Option.flatMap(getField(call, key), Stdlib_JSON.Decode.string);
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
function descriptorField(call, key) {
|
|
88
|
+
return Stdlib_Option.flatMap(getField(call, "descriptor"), d => getField(d, key));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function descriptorStr(call, key) {
|
|
92
|
+
return Stdlib_Option.flatMap(descriptorField(call, key), Stdlib_JSON.Decode.string);
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
function calls(j) {
|
|
80
96
|
return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(j), []);
|
|
81
97
|
}
|
|
@@ -88,20 +104,29 @@ globalThis.describe("withLiveUpdates (B3.3a Postgres live updates)", () => {
|
|
|
88
104
|
let call = arr[0];
|
|
89
105
|
globalThis.expect(str(call, "entityKey")).toEqual("p1");
|
|
90
106
|
globalThis.expect(str(call, "changeKind")).toEqual("Updated");
|
|
91
|
-
globalThis.expect(
|
|
107
|
+
globalThis.expect(descriptorStr(call, "sortKeyValue")).toEqual("2024-01-02");
|
|
92
108
|
globalThis.expect(str(call, "topicName")).toEqual("Shop_Orders");
|
|
109
|
+
globalThis.expect(descriptorField(call, "state")).toEqual(getField(call, "state"));
|
|
110
|
+
globalThis.expect(descriptorStr(call, "seq")).toEqual("1");
|
|
93
111
|
});
|
|
94
112
|
globalThis.test("save on a composite table uses id-subKey and createdAt fallback", async () => {
|
|
95
113
|
let c = await run("save-composite");
|
|
96
114
|
let call = calls(c)[0];
|
|
97
115
|
globalThis.expect(str(call, "entityKey")).toEqual("o1-L2");
|
|
98
116
|
globalThis.expect(str(call, "changeKind")).toEqual("Updated");
|
|
99
|
-
globalThis.expect(
|
|
117
|
+
globalThis.expect(descriptorStr(call, "sortKeyValue")).toEqual("2024-03-04");
|
|
100
118
|
});
|
|
101
119
|
globalThis.test("save without updatedAt/createdAt omits sortKeyValue", async () => {
|
|
102
120
|
let c = await run("save-no-sortkey");
|
|
103
121
|
let call = calls(c)[0];
|
|
104
|
-
globalThis.expect(
|
|
122
|
+
globalThis.expect(descriptorField(call, "sortKeyValue")).toEqual(undefined);
|
|
123
|
+
});
|
|
124
|
+
globalThis.test("an oversized row degrades to a metadata-only descriptor", async () => {
|
|
125
|
+
let c = await run("save-oversized");
|
|
126
|
+
let call = calls(c)[0];
|
|
127
|
+
globalThis.expect(descriptorField(call, "state")).toEqual(undefined);
|
|
128
|
+
globalThis.expect(descriptorStr(call, "changeKind")).toEqual("Updated");
|
|
129
|
+
globalThis.expect(descriptorStr(call, "seq")).toEqual("1");
|
|
105
130
|
});
|
|
106
131
|
globalThis.test("saveBatch publishes one Updated per item", async () => {
|
|
107
132
|
let c = await run("saveBatch");
|
|
@@ -153,6 +178,8 @@ export {
|
|
|
153
178
|
run,
|
|
154
179
|
getField,
|
|
155
180
|
str,
|
|
181
|
+
descriptorField,
|
|
182
|
+
descriptorStr,
|
|
156
183
|
calls,
|
|
157
184
|
}
|
|
158
185
|
/* run Not a pure module */
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Harness for StateChangeDescriptorParityTest.
|
|
2
|
+
//
|
|
3
|
+
// The live-update change descriptor has three independent implementations and no
|
|
4
|
+
// shared code (the DynamoDB relay's module is deliberately Pulumi-free, so it
|
|
5
|
+
// cannot import a core builder without risking deploy-time code in its Lambda
|
|
6
|
+
// graph). This harness drives all three through the same logical changes and
|
|
7
|
+
// hands the results back for comparison.
|
|
8
|
+
//
|
|
9
|
+
// Plain ESM rather than ReScript because two of the three modules live outside
|
|
10
|
+
// this package and one is hand-written JS; the assertions stay in the .res test.
|
|
11
|
+
|
|
12
|
+
const LOCAL = "../../local/src/adapter/LocalStateChangeDescriptor.res.mjs";
|
|
13
|
+
const RELAY = "../src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs";
|
|
14
|
+
const POSTGRES = "../src/adapter/Runtime/StateTopicPublish.mjs";
|
|
15
|
+
|
|
16
|
+
// Each case is one logical change expressed three ways. `changeKind` is already
|
|
17
|
+
// normalised: the three sites derive it differently (local compares against the
|
|
18
|
+
// stored row, the relay maps the stream eventName, Postgres has only upserts), and
|
|
19
|
+
// that derivation is covered by their own tests — what is asserted here is that
|
|
20
|
+
// the same logical change produces the same descriptor.
|
|
21
|
+
const CASES = [
|
|
22
|
+
{
|
|
23
|
+
name: "single-key save with updatedAt",
|
|
24
|
+
changeKind: "Updated",
|
|
25
|
+
entityKey: "p1",
|
|
26
|
+
state: { id: "p1", name: "Widget", updatedAt: "2026-05-19T12:00:00Z" },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "composite-key save falling back to createdAt",
|
|
30
|
+
changeKind: "Updated",
|
|
31
|
+
entityKey: "o1-L2",
|
|
32
|
+
state: { id: "o1", lineId: "L2", createdAt: "2026-03-04T08:00:00Z" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "save with no sort timestamp",
|
|
36
|
+
changeKind: "Updated",
|
|
37
|
+
entityKey: "p1",
|
|
38
|
+
state: { id: "p1", name: "Widget" },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "first save of a key",
|
|
42
|
+
changeKind: "Added",
|
|
43
|
+
entityKey: "p2",
|
|
44
|
+
state: { id: "p2", name: "Gadget", updatedAt: "2026-05-19T12:00:00Z" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "oversized row degrades to metadata only",
|
|
48
|
+
changeKind: "Updated",
|
|
49
|
+
entityKey: "p3",
|
|
50
|
+
state: { id: "p3", blob: "x".repeat(70 * 1024), updatedAt: "2026-05-19T12:00:00Z" },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "delete carries no row",
|
|
54
|
+
changeKind: "Removed",
|
|
55
|
+
entityKey: "p1",
|
|
56
|
+
state: undefined,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build every case with every implementation.
|
|
62
|
+
*
|
|
63
|
+
* Returns `[{ name, local, relay, postgres }]`, each descriptor a plain object
|
|
64
|
+
* with `seq` replaced by the literal "<seq>" — the three sites take their
|
|
65
|
+
* sequence from different monotonic sources by design (stream SequenceNumber vs
|
|
66
|
+
* a wall-clock counter), so the values differ while the field's presence and
|
|
67
|
+
* type must not.
|
|
68
|
+
*/
|
|
69
|
+
export async function buildAll() {
|
|
70
|
+
const local = await import(LOCAL);
|
|
71
|
+
const relay = await import(RELAY);
|
|
72
|
+
const postgres = await import(POSTGRES);
|
|
73
|
+
|
|
74
|
+
const normalise = (descriptor) => {
|
|
75
|
+
const seq = descriptor.seq;
|
|
76
|
+
if (typeof seq !== "string" || seq.length === 0) {
|
|
77
|
+
throw new Error("descriptor is missing a string seq: " + JSON.stringify(descriptor));
|
|
78
|
+
}
|
|
79
|
+
return { ...descriptor, seq: "<seq>" };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return CASES.map(({ name, changeKind, entityKey, state }) => ({
|
|
83
|
+
name,
|
|
84
|
+
local: normalise(
|
|
85
|
+
local.make(changeKind, entityKey, state === undefined ? undefined : state, local.nextSequence()),
|
|
86
|
+
),
|
|
87
|
+
relay: normalise(
|
|
88
|
+
relay.makeDescriptor(
|
|
89
|
+
changeKind,
|
|
90
|
+
entityKey,
|
|
91
|
+
// The relay unmarshalls the stream image before building the descriptor,
|
|
92
|
+
// so it takes the same plain row the other two do. A REMOVE is handed the
|
|
93
|
+
// OldImage, which makeDescriptor then drops.
|
|
94
|
+
state === undefined ? { id: entityKey } : state,
|
|
95
|
+
"49590300000000016818000000",
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
postgres: normalise(
|
|
99
|
+
postgres.makeDescriptor({ changeKind, entityKey, state, seq: postgres.nextSequence() }),
|
|
100
|
+
),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~owner, ~opts) => {
|
|
2
|
-
let tags = AWS.Tags.make(~name, ~kind=ReventlessCore.EventLog.componentType, ~role=EventLog, ~owner?)
|
|
3
|
-
let table = Util.DynamoDb.makeTable(
|
|
4
|
-
name,
|
|
5
|
-
~attributes=[{name: "id", type_: "S"}, {name: "position", type_: "S"}],
|
|
6
|
-
~rangeKey="position",
|
|
7
|
-
~tags,
|
|
8
|
-
~opts,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
resources: [table->Util_DynamoDb.toResource(~tags=tags->Pulumi.Output.fromInput)],
|
|
13
|
-
operations: table
|
|
14
|
-
->Util_DynamoDb.toResolvedTableOutput
|
|
15
|
-
->Pulumi.Output.apply(resolvedTable => {
|
|
16
|
-
ReventlessCore.EventLog_Adapter.append: EventLogStorage_DynamoDb_Runtime.append(
|
|
17
|
-
resolvedTable,
|
|
18
|
-
...
|
|
19
|
-
),
|
|
20
|
-
replay: EventLogStorage_DynamoDb_Runtime.replay(resolvedTable, ...),
|
|
21
|
-
replayStream: EventLogStorage_DynamoDb_Runtime.replayStream(resolvedTable),
|
|
22
|
-
appendStream: EventLogStorage_DynamoDb_Runtime.appendStream(resolvedTable, ...),
|
|
23
|
-
latestSnapshot: EventLogStorage_DynamoDb_Runtime.latestSnapshot(resolvedTable),
|
|
24
|
-
writeSnapshot: EventLogStorage_DynamoDb_Runtime.writeSnapshot(resolvedTable),
|
|
25
|
-
}),
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
4
|
-
import * as EventLog$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventLog/EventLog.res.mjs";
|
|
5
|
-
import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
|
|
6
|
-
import * as EventLogStorage_DynamoDb_Runtime$ReventlessAws from "./EventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
7
|
-
|
|
8
|
-
function make(name, owner, opts) {
|
|
9
|
-
let tags = AWS_Tags$ReventlessAws.make(name, EventLog$ReventlessCore.componentType, "EventLog", undefined, undefined, owner, undefined, undefined);
|
|
10
|
-
let table = Util_DynamoDb$ReventlessAws.makeTable([
|
|
11
|
-
{
|
|
12
|
-
name: "id",
|
|
13
|
-
type: "S"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: "position",
|
|
17
|
-
type: "S"
|
|
18
|
-
}
|
|
19
|
-
], undefined, undefined, "position", tags, opts, name);
|
|
20
|
-
return {
|
|
21
|
-
resources: [Util_DynamoDb$ReventlessAws.toResource(tags, table)],
|
|
22
|
-
operations: Util_DynamoDb$ReventlessAws.toResolvedTableOutput(table).apply(resolvedTable => ({
|
|
23
|
-
append: EventLogStorage_DynamoDb_Runtime$ReventlessAws.append(resolvedTable),
|
|
24
|
-
replay: EventLogStorage_DynamoDb_Runtime$ReventlessAws.replay(resolvedTable),
|
|
25
|
-
replayStream: EventLogStorage_DynamoDb_Runtime$ReventlessAws.replayStream(resolvedTable),
|
|
26
|
-
appendStream: EventLogStorage_DynamoDb_Runtime$ReventlessAws.appendStream(resolvedTable),
|
|
27
|
-
latestSnapshot: EventLogStorage_DynamoDb_Runtime$ReventlessAws.latestSnapshot(resolvedTable),
|
|
28
|
-
writeSnapshot: EventLogStorage_DynamoDb_Runtime$ReventlessAws.writeSnapshot(resolvedTable)
|
|
29
|
-
}))
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export {
|
|
34
|
-
make,
|
|
35
|
-
}
|
|
36
|
-
/* AWS_Tags-ReventlessAws Not a pure module */
|