@reventlessdev/reventless-core 3.0.0-alpha.137 → 3.0.0-alpha.138
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 +7 -0
- package/package.json +5 -5
- package/src/components/Aggregate/Aggregate_Callback.res +158 -12
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +131 -9
- package/tests/aggregate/AggregateSnapshotTest.res +357 -0
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +645 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.138 (2026-07-04)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** wire aggregate snapshots into the command callback (aggregate-snapshotting plan, step 5) ([09fda78](https://github.com/ReventlessDev/reventless-core/commit/09fda78a19bb758482111cebedfdb115d0ec7a35))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.137 (2026-07-03)
|
|
7
14
|
|
|
8
15
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.138",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-
|
|
32
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
|
|
31
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
|
|
33
32
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.5",
|
|
34
33
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
|
|
35
|
-
"@reventlessdev/rescript-
|
|
36
|
-
"@reventlessdev/rescript-
|
|
34
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.24",
|
|
35
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
|
|
37
36
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
|
|
37
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.13",
|
|
39
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.23",
|
|
40
40
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.64",
|
|
@@ -122,6 +122,154 @@ module Make = (
|
|
|
122
122
|
|
|
123
123
|
let resetCache = () => replayCache->Lru.clear
|
|
124
124
|
|
|
125
|
+
// Persisted-snapshot configuration (docs/plans/aggregate-snapshotting.md).
|
|
126
|
+
// `None` (the default) keeps full replay; `Some({interval, stateSchema})`
|
|
127
|
+
// seeds cold replays from the latest persisted snapshot and writes a fresh
|
|
128
|
+
// one every `interval` events. Snapshots are a read optimization only — the
|
|
129
|
+
// OCC append remains the sole consistency primitive, so a missing, drifted,
|
|
130
|
+
// or corrupt snapshot degrades silently to full replay.
|
|
131
|
+
let snapshotConfig = Behavior.snapshot
|
|
132
|
+
|
|
133
|
+
// Structural hash of the state schema, computed once. Stored on every written
|
|
134
|
+
// snapshot and compared on read: a snapshot whose hash differs from the
|
|
135
|
+
// current schema (a state-shape change since it was written) is ignored and
|
|
136
|
+
// overwritten at the next boundary, so a redeploy that changes `type state`
|
|
137
|
+
// can't decode stale bytes into the wrong shape. NOTE: this catches *shape*
|
|
138
|
+
// drift only — a semantics-only `evolve` change with an unchanged state type
|
|
139
|
+
// is invisible (see the plan's open question; for alpha, wipe snapshots on
|
|
140
|
+
// such changes, consistent with the alpha-wipe-over-migration convention).
|
|
141
|
+
let stateSchemaHash = switch snapshotConfig {
|
|
142
|
+
| Some({stateSchema}) =>
|
|
143
|
+
HashObj.hashDict(
|
|
144
|
+
~dict=Dict.fromArray([("state", SchemaWalker.describeSchema(stateSchema->Obj.magic))]),
|
|
145
|
+
~options={algorithm: SHA256},
|
|
146
|
+
)
|
|
147
|
+
| None => ""
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// State ⇄ JSON via the configured stateSchema. Both swallow sury failures to
|
|
151
|
+
// `None` — an unserializable state just skips the snapshot write, and an
|
|
152
|
+
// undecodable snapshot falls back to full replay; neither can fail a command.
|
|
153
|
+
let encodeState = (state: Behavior.state): option<JSON.t> =>
|
|
154
|
+
switch snapshotConfig {
|
|
155
|
+
| Some({stateSchema}) =>
|
|
156
|
+
switch state->S.reverseConvertToJsonOrThrow(stateSchema) {
|
|
157
|
+
| json => Some(json)
|
|
158
|
+
| exception _ => None
|
|
159
|
+
}
|
|
160
|
+
| None => None
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let decodeState = (json: JSON.t): option<Behavior.state> =>
|
|
164
|
+
switch snapshotConfig {
|
|
165
|
+
| Some({stateSchema}) =>
|
|
166
|
+
switch json->S.parseJsonOrThrow(stateSchema) {
|
|
167
|
+
| state => Some(state)
|
|
168
|
+
| exception _ => None
|
|
169
|
+
}
|
|
170
|
+
| None => None
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Cold read (in-process cache miss): when snapshots are enabled, seed from the
|
|
174
|
+
// latest persisted snapshot (hash-gated) and replay only the events after it;
|
|
175
|
+
// otherwise replay full history from seq 0. Returns `(state, sequenceNr)` where
|
|
176
|
+
// sequenceNr is the total event count (the OCC condition for the next append).
|
|
177
|
+
let coldReadState = id => {
|
|
178
|
+
let idStr = id->Spec.Id.toString
|
|
179
|
+
let seedEffect = switch snapshotConfig {
|
|
180
|
+
| None => Effect.succeed((Behavior.initialState, 0))
|
|
181
|
+
| Some(_) =>
|
|
182
|
+
Effect.promise(() => Ops.eventLog.latestSnapshot(id))
|
|
183
|
+
->Effect.map(snapResult =>
|
|
184
|
+
switch snapResult {
|
|
185
|
+
| Ok(Some(snap)) if snap.EventLog.schemaHash == stateSchemaHash =>
|
|
186
|
+
switch decodeState(snap.state) {
|
|
187
|
+
| Some(state) => (state, snap.seqNr)
|
|
188
|
+
| None =>
|
|
189
|
+
EffectLogger.logWarn(
|
|
190
|
+
~comp,
|
|
191
|
+
`snapshot ignored (undecodable): id=${idStr}, seq=${snap.seqNr->Int.toString} — full replay`,
|
|
192
|
+
)->Effect.runSync
|
|
193
|
+
(Behavior.initialState, 0)
|
|
194
|
+
}
|
|
195
|
+
| Ok(Some(snap)) =>
|
|
196
|
+
EffectLogger.logWarn(
|
|
197
|
+
~comp,
|
|
198
|
+
`snapshot ignored (schema drift): id=${idStr}, seq=${snap.EventLog.seqNr->Int.toString} — full replay`,
|
|
199
|
+
)->Effect.runSync
|
|
200
|
+
(Behavior.initialState, 0)
|
|
201
|
+
| Ok(None) => (Behavior.initialState, 0)
|
|
202
|
+
| Error(msg) =>
|
|
203
|
+
EffectLogger.logWarn(
|
|
204
|
+
~comp,
|
|
205
|
+
`snapshot read failed (ignored): id=${idStr}: ${msg} — full replay`,
|
|
206
|
+
)->Effect.runSync
|
|
207
|
+
(Behavior.initialState, 0)
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
seedEffect->Effect.flatMap(((seedState, seedSeq)) =>
|
|
212
|
+
Ops.eventLog.replayStream(id, ~fromSeq=seedSeq)
|
|
213
|
+
->Stream.runFold((seedState, seedSeq), ((st, n), ev) => (Behavior.evolve(st, ev), n + 1))
|
|
214
|
+
->Effect.tap(((_, n)) => {
|
|
215
|
+
let detail =
|
|
216
|
+
seedSeq > 0
|
|
217
|
+
? `(snapshot@${seedSeq->Int.toString}, ${(n - seedSeq)->Int.toString} delta event(s))`
|
|
218
|
+
: `${n->Int.toString} event(s)`
|
|
219
|
+
EffectLogger.logInfo(~comp, `replay: id=${idStr}, ${detail}`)
|
|
220
|
+
})
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fire-and-forget snapshot write — never awaited, never fails the command. A
|
|
225
|
+
// failed write just means the next cold replay reads a longer delta.
|
|
226
|
+
let fireSnapshotWrite = (id, idStr, snap: EventLog.snapshot) => {
|
|
227
|
+
let _ =
|
|
228
|
+
Ops.eventLog.writeSnapshot(id, snap)
|
|
229
|
+
->Promise.then(result => {
|
|
230
|
+
switch result {
|
|
231
|
+
| Ok() =>
|
|
232
|
+
EffectLogger.logDebug(
|
|
233
|
+
~comp,
|
|
234
|
+
`snapshot written: id=${idStr}, seq=${snap.seqNr->Int.toString}`,
|
|
235
|
+
)->Effect.runSync
|
|
236
|
+
| Error(msg) =>
|
|
237
|
+
EffectLogger.logWarn(
|
|
238
|
+
~comp,
|
|
239
|
+
`snapshot write failed (ignored): id=${idStr}: ${msg}`,
|
|
240
|
+
)->Effect.runSync
|
|
241
|
+
}
|
|
242
|
+
Promise.resolve()
|
|
243
|
+
})
|
|
244
|
+
->Promise.catch(err => {
|
|
245
|
+
let msg = err->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
|
|
246
|
+
EffectLogger.logWarn(
|
|
247
|
+
~comp,
|
|
248
|
+
`snapshot write threw (ignored): id=${idStr}: ${msg}`,
|
|
249
|
+
)->Effect.runSync
|
|
250
|
+
Promise.resolve()
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Write a snapshot when this append crossed an `interval` boundary (a multiple
|
|
255
|
+
// of interval lies in (oldSeq, newSeq]). Keep-one: on a batch that crosses
|
|
256
|
+
// several boundaries we still write just the latest state at newSeq. No-op
|
|
257
|
+
// when snapshots are disabled or the state can't be serialized.
|
|
258
|
+
let maybeWriteSnapshot = (id, idStr, ~oldSeq, ~newSeq, state) =>
|
|
259
|
+
switch snapshotConfig {
|
|
260
|
+
| Some({interval}) if interval > 0 && newSeq / interval > oldSeq / interval =>
|
|
261
|
+
switch encodeState(state) {
|
|
262
|
+
| Some(stateJson) =>
|
|
263
|
+
fireSnapshotWrite(id, idStr, {EventLog.seqNr: newSeq, state: stateJson, schemaHash: stateSchemaHash})
|
|
264
|
+
| None =>
|
|
265
|
+
EffectLogger.logWarn(
|
|
266
|
+
~comp,
|
|
267
|
+
`snapshot skipped (state not serializable): id=${idStr}, seq=${newSeq->Int.toString}`,
|
|
268
|
+
)->Effect.runSync
|
|
269
|
+
}
|
|
270
|
+
| _ => ()
|
|
271
|
+
}
|
|
272
|
+
|
|
125
273
|
// Reports per-command outcomes on the inline side-channels and produces the
|
|
126
274
|
// per-reference Ok/Error array consumed by SQS-style consumers. Domain rejections
|
|
127
275
|
// are always Ok(reference) — SQS deletes the message because retry would not help —
|
|
@@ -160,20 +308,17 @@ module Make = (
|
|
|
160
308
|
) => {
|
|
161
309
|
let idStr = id->Spec.Id.toString
|
|
162
310
|
|
|
163
|
-
// Warm path: seed from the
|
|
164
|
-
// OCC append below fences any staleness. Cold
|
|
311
|
+
// Warm path: seed from the in-process cache and skip both the snapshot read
|
|
312
|
+
// and the event-log replay; the OCC append below fences any staleness. Cold
|
|
313
|
+
// path (`coldReadState`): seed from the persisted snapshot when enabled, else
|
|
314
|
+
// full replay.
|
|
165
315
|
let readState = switch replayCache->Lru.get(idStr) {
|
|
166
316
|
| Some((state, seqNr)) =>
|
|
167
317
|
EffectLogger.logInfo(
|
|
168
318
|
~comp,
|
|
169
319
|
`replay skipped (cached): id=${idStr}, seq=${seqNr->Int.toString}`,
|
|
170
320
|
)->Effect.map(_ => (state, seqNr))
|
|
171
|
-
| None =>
|
|
172
|
-
Ops.eventLog.replayStream(id)
|
|
173
|
-
->Stream.runFold((Behavior.initialState, 0), ((st, n), ev) => (Behavior.evolve(st, ev), n + 1))
|
|
174
|
-
->Effect.tap(((_, n)) =>
|
|
175
|
-
EffectLogger.logInfo(~comp, `replay: id=${idStr}, ${n->Int.toString} event(s)`)
|
|
176
|
-
)
|
|
321
|
+
| None => coldReadState(id)
|
|
177
322
|
}
|
|
178
323
|
|
|
179
324
|
readState
|
|
@@ -222,10 +367,11 @@ module Make = (
|
|
|
222
367
|
| Ok(_) =>
|
|
223
368
|
// The post-decide fold state IS the post-append replay result;
|
|
224
369
|
// cache it so the next command for this id skips the replay.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
370
|
+
let newSeq = sequenceNr + generatedEvents'->Array.length
|
|
371
|
+
replayCache->Lru.put(idStr, (finalState, newSeq))
|
|
372
|
+
// Persist a snapshot if this append crossed an interval boundary
|
|
373
|
+
// (fire-and-forget — never blocks or fails the command).
|
|
374
|
+
maybeWriteSnapshot(id, idStr, ~oldSeq=sequenceNr, ~newSeq, finalState)
|
|
229
375
|
let perRef = reportFinalOutcomes(
|
|
230
376
|
outcomes,
|
|
231
377
|
~entityId=idStr,
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import HashObject from "hash-object";
|
|
3
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
4
8
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
9
|
import * as Effect from "effect/Effect";
|
|
6
10
|
import * as Stream from "effect/Stream";
|
|
11
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
12
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
13
|
import * as Lru$ReventlessCore from "../../util/Lru.res.mjs";
|
|
8
14
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
9
15
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
10
16
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
17
|
+
import * as SchemaWalker$ReventlessCore from "../../plugin/component/SchemaWalker.res.mjs";
|
|
11
18
|
import * as CommandTopic_Helpers$ReventlessCore from "../CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
12
19
|
|
|
13
20
|
function Make(Spec) {
|
|
@@ -67,6 +74,125 @@ function Make(Spec) {
|
|
|
67
74
|
};
|
|
68
75
|
let replayCache = Lru$ReventlessCore.make(100);
|
|
69
76
|
let resetCache = () => Lru$ReventlessCore.clear(replayCache);
|
|
77
|
+
let snapshotConfig = Behavior.snapshot;
|
|
78
|
+
let stateSchemaHash = snapshotConfig !== undefined ? HashObject(Object.fromEntries([[
|
|
79
|
+
"state",
|
|
80
|
+
SchemaWalker$ReventlessCore.describeSchema(snapshotConfig.stateSchema)
|
|
81
|
+
]]), {
|
|
82
|
+
algorithm: "sha256"
|
|
83
|
+
}) : "";
|
|
84
|
+
let encodeState = state => {
|
|
85
|
+
if (snapshotConfig === undefined) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let json;
|
|
89
|
+
try {
|
|
90
|
+
json = S.reverseConvertToJsonOrThrow(state, snapshotConfig.stateSchema);
|
|
91
|
+
} catch (exn) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return json;
|
|
95
|
+
};
|
|
96
|
+
let decodeState = json => {
|
|
97
|
+
if (snapshotConfig === undefined) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let state;
|
|
101
|
+
try {
|
|
102
|
+
state = S.parseJsonOrThrow(json, snapshotConfig.stateSchema);
|
|
103
|
+
} catch (exn) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
return Primitive_option.some(state);
|
|
107
|
+
};
|
|
108
|
+
let coldReadState = id => {
|
|
109
|
+
let idStr = Spec.Id.toString(id);
|
|
110
|
+
let seedEffect = snapshotConfig !== undefined ? Effect.map(Effect.promise(() => Ops.eventLog.latestSnapshot(id)), snapResult => {
|
|
111
|
+
if (snapResult.TAG === "Ok") {
|
|
112
|
+
let snap = snapResult._0;
|
|
113
|
+
if (snap === undefined) {
|
|
114
|
+
return [
|
|
115
|
+
Behavior.initialState,
|
|
116
|
+
0
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
if (snap.schemaHash === stateSchemaHash) {
|
|
120
|
+
let state = decodeState(snap.state);
|
|
121
|
+
if (state !== undefined) {
|
|
122
|
+
return [
|
|
123
|
+
Primitive_option.valFromOption(state),
|
|
124
|
+
snap.seqNr
|
|
125
|
+
];
|
|
126
|
+
} else {
|
|
127
|
+
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot ignored (undecodable): id=` + idStr + `, seq=` + snap.seqNr.toString() + ` — full replay`));
|
|
128
|
+
return [
|
|
129
|
+
Behavior.initialState,
|
|
130
|
+
0
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot ignored (schema drift): id=` + idStr + `, seq=` + snap.seqNr.toString() + ` — full replay`));
|
|
135
|
+
return [
|
|
136
|
+
Behavior.initialState,
|
|
137
|
+
0
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot read failed (ignored): id=` + idStr + `: ` + snapResult._0 + ` — full replay`));
|
|
141
|
+
return [
|
|
142
|
+
Behavior.initialState,
|
|
143
|
+
0
|
|
144
|
+
];
|
|
145
|
+
}) : Effect.succeed([
|
|
146
|
+
Behavior.initialState,
|
|
147
|
+
0
|
|
148
|
+
]);
|
|
149
|
+
return Effect.flatMap(seedEffect, param => {
|
|
150
|
+
let seedSeq = param[1];
|
|
151
|
+
return Effect.tap(Stream.runFold(Ops.eventLog.replayStream(id, seedSeq), [
|
|
152
|
+
param[0],
|
|
153
|
+
seedSeq
|
|
154
|
+
], (param, ev) => [
|
|
155
|
+
Behavior.evolve(param[0], ev),
|
|
156
|
+
param[1] + 1 | 0
|
|
157
|
+
]), param => {
|
|
158
|
+
let n = param[1];
|
|
159
|
+
let detail = seedSeq > 0 ? `(snapshot@` + seedSeq.toString() + `, ` + (n - seedSeq | 0).toString() + ` delta event(s))` : n.toString() + ` event(s)`;
|
|
160
|
+
return EffectLogger$ReventlessCore.logInfo(comp, undefined, `replay: id=` + idStr + `, ` + detail);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
let maybeWriteSnapshot = (id, idStr, oldSeq, newSeq, state) => {
|
|
165
|
+
if (snapshotConfig === undefined) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let interval = snapshotConfig.interval;
|
|
169
|
+
if (!(interval > 0 && Primitive_int.div(newSeq, interval) > Primitive_int.div(oldSeq, interval))) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let stateJson = encodeState(state);
|
|
173
|
+
if (stateJson !== undefined) {
|
|
174
|
+
let snap = {
|
|
175
|
+
seqNr: newSeq,
|
|
176
|
+
state: stateJson,
|
|
177
|
+
schemaHash: stateSchemaHash
|
|
178
|
+
};
|
|
179
|
+
Stdlib_Promise.$$catch(Ops.eventLog.writeSnapshot(id, snap).then(result => {
|
|
180
|
+
if (result.TAG === "Ok") {
|
|
181
|
+
Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `snapshot written: id=` + idStr + `, seq=` + snap.seqNr.toString()));
|
|
182
|
+
} else {
|
|
183
|
+
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot write failed (ignored): id=` + idStr + `: ` + result._0));
|
|
184
|
+
}
|
|
185
|
+
return Promise.resolve();
|
|
186
|
+
}), err => {
|
|
187
|
+
let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(err), Stdlib_JsExn.message), "unknown");
|
|
188
|
+
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot write threw (ignored): id=` + idStr + `: ` + msg));
|
|
189
|
+
return Promise.resolve();
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
} else {
|
|
193
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `snapshot skipped (state not serializable): id=` + idStr + `, seq=` + newSeq.toString()));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
70
196
|
let reportFinalOutcomes = (outcomes, entityId, appendSucceeded, appendedEventCount, appendErrorDetailOpt) => {
|
|
71
197
|
let appendErrorDetail = appendErrorDetailOpt !== undefined ? appendErrorDetailOpt : "";
|
|
72
198
|
return outcomes.map(param => {
|
|
@@ -115,13 +241,7 @@ function Make(Spec) {
|
|
|
115
241
|
seqNr
|
|
116
242
|
]);
|
|
117
243
|
} else {
|
|
118
|
-
readState =
|
|
119
|
-
Behavior.initialState,
|
|
120
|
-
0
|
|
121
|
-
], (param, ev) => [
|
|
122
|
-
Behavior.evolve(param[0], ev),
|
|
123
|
-
param[1] + 1 | 0
|
|
124
|
-
]), param => EffectLogger$ReventlessCore.logInfo(comp, undefined, `replay: id=` + idStr + `, ` + param[1].toString() + ` event(s)`));
|
|
244
|
+
readState = coldReadState(id);
|
|
125
245
|
}
|
|
126
246
|
return Effect.flatMap(readState, param => {
|
|
127
247
|
let sequenceNr = param[1];
|
|
@@ -156,10 +276,12 @@ function Make(Spec) {
|
|
|
156
276
|
let eventJsons = eventsToAppend.map(event$p => Message$ReventlessCore.encode(event$p.event, Spec.eventSchema));
|
|
157
277
|
return Effect.flatMap(Effect.flatMap(EffectLogger$ReventlessCore.logInfo(comp, eventJsons, `produced ` + eventCount + ` event(s): [` + eventDetails + `]`), () => Effect.promise(() => Ops.eventLog.append(sequenceNr, id, eventsToAppend))), appendResult => {
|
|
158
278
|
if (appendResult.TAG === "Ok") {
|
|
279
|
+
let newSeq = sequenceNr + eventsToAppend.length | 0;
|
|
159
280
|
Lru$ReventlessCore.put(replayCache, idStr, [
|
|
160
281
|
finalState,
|
|
161
|
-
|
|
282
|
+
newSeq
|
|
162
283
|
]);
|
|
284
|
+
maybeWriteSnapshot(id, idStr, sequenceNr, newSeq, finalState);
|
|
163
285
|
let perRef = reportFinalOutcomes(outcomes, idStr, true, eventsToAppend.length, undefined);
|
|
164
286
|
return Effect.map(EffectLogger$ReventlessCore.logInfo(comp, undefined, `append: id=` + idStr), () => ({
|
|
165
287
|
TAG: "Ok",
|
|
@@ -234,4 +356,4 @@ function Make(Spec) {
|
|
|
234
356
|
export {
|
|
235
357
|
Make,
|
|
236
358
|
}
|
|
237
|
-
/*
|
|
359
|
+
/* S Not a pure module */
|