@reventlessdev/reventless-local 3.0.0-alpha.300 → 3.0.0-alpha.302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +11 -11
  3. package/src/EmitCapabilities.res +12 -5
  4. package/src/EmitCapabilities.res.mjs +13 -1
  5. package/src/Platform.res +6 -7
  6. package/src/Platform.res.mjs +10 -12
  7. package/src/adapter/Auth/{UserStore.res → AccountStore.res} +17 -17
  8. package/src/adapter/Auth/{UserStore.res.mjs → AccountStore.res.mjs} +11 -11
  9. package/src/adapter/Auth/LocalAuth.res +5 -5
  10. package/src/adapter/LocalCapabilities.res +2 -2
  11. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  12. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -1
  13. package/src/capability/{Capability_IdentityProvider_UserStore.res → Capability_IdentityProvider_AccountStore.res} +1 -1
  14. package/tests/EmitCapabilitiesTest.res +6 -0
  15. package/tests/EmitCapabilitiesTest.res.mjs +6 -0
  16. package/tests/adapter/{LocalAuthUserStoreTest.res → LocalAuthAccountStoreTest.res} +48 -48
  17. package/tests/adapter/{LocalAuthUserStoreTest.res.mjs → LocalAuthAccountStoreTest.res.mjs} +44 -44
  18. package/tests/capability/{Capability_IdentityProvider_UserStoreTest.res → Capability_IdentityProvider_AccountStoreTest.res} +15 -13
  19. package/tests/capability/{Capability_IdentityProvider_UserStoreTest.res.mjs → Capability_IdentityProvider_AccountStoreTest.res.mjs} +15 -15
  20. package/tests/capability/LocalCapabilitiesTest.res +2 -2
  21. package/tests/capability/LocalCapabilitiesTest.res.mjs +3 -3
  22. package/tests/components/dcb/DcbRepeatTest.res +291 -0
  23. package/tests/components/dcb/DcbRepeatTest.res.mjs +420 -0
  24. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +71 -15
  25. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +119 -14
  26. package/tests/plugin/Plugin_Fixtures.res +1 -3
  27. package/tests/plugin/Plugin_Fixtures.res.mjs +1 -2
  28. /package/src/capability/{Capability_IdentityProvider_UserStore.res.mjs → Capability_IdentityProvider_AccountStore.res.mjs} +0 -0
@@ -7,9 +7,9 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
8
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
9
  import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
10
- import * as UserStore$ReventlessLocal from "../../src/adapter/Auth/UserStore.res.mjs";
11
10
  import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
12
- import * as Capability_IdentityProvider_UserStore$ReventlessLocal from "../../src/capability/Capability_IdentityProvider_UserStore.res.mjs";
11
+ import * as AccountStore$ReventlessLocal from "../../src/adapter/Auth/AccountStore.res.mjs";
12
+ import * as Capability_IdentityProvider_AccountStore$ReventlessLocal from "../../src/capability/Capability_IdentityProvider_AccountStore.res.mjs";
13
13
 
14
14
  function email(address) {
15
15
  return {
@@ -20,18 +20,18 @@ function email(address) {
20
20
 
21
21
  function freshFile() {
22
22
  let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-identity-"));
23
- return Nodepath.join(dir, ".reventless", "users.yaml");
23
+ return Nodepath.join(dir, ".reventless", "accounts.yaml");
24
24
  }
25
25
 
26
26
  function resetAll() {
27
27
  LocalAuth$ReventlessLocal.resetUsers();
28
28
  LocalAuth$ReventlessLocal.Login.resetStore();
29
- UserStore$ReventlessLocal.resetResolution();
29
+ AccountStore$ReventlessLocal.resetResolution();
30
30
  }
31
31
 
32
32
  function restartFrom(file) {
33
33
  resetAll();
34
- let message = UserStore$ReventlessLocal.load(undefined, file, undefined);
34
+ let message = AccountStore$ReventlessLocal.load(undefined, file, undefined);
35
35
  if (message.TAG === "Ok") {
36
36
  return;
37
37
  } else {
@@ -39,9 +39,9 @@ function restartFrom(file) {
39
39
  }
40
40
  }
41
41
 
42
- globalThis.describe("Capability_IdentityProvider_UserStore", () => {
42
+ globalThis.describe("Capability_IdentityProvider_AccountStore", () => {
43
43
  globalThis.test("publishes all four operations", () => {
44
- globalThis.expect(Capability_IdentityProvider_UserStore$ReventlessLocal.make(freshFile(), undefined).operations).toEqual([
44
+ globalThis.expect(Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined).operations).toEqual([
45
45
  "CreatePrincipal",
46
46
  "AddToGroup",
47
47
  "RemoveFromGroup",
@@ -51,7 +51,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
51
51
  globalThis.test("a created principal can log in now and after a restart", async () => {
52
52
  resetAll();
53
53
  let file = freshFile();
54
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(file, undefined);
54
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
55
55
  let created = await provider.createPrincipal({
56
56
  TAG: "ToEmail",
57
57
  _0: "new@example.com"
@@ -75,7 +75,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
75
75
  });
76
76
  globalThis.test("the same contact twice is a Conflict, not a second account", async () => {
77
77
  resetAll();
78
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(freshFile(), undefined);
78
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
79
79
  let create = () => provider.createPrincipal({
80
80
  TAG: "ToEmail",
81
81
  _0: "twice@example.com"
@@ -88,7 +88,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
88
88
  });
89
89
  globalThis.test("NoCredential writes an account no password opens", async () => {
90
90
  resetAll();
91
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(freshFile(), undefined);
91
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
92
92
  await provider.createPrincipal({
93
93
  TAG: "ToEmail",
94
94
  _0: "later@example.com"
@@ -97,7 +97,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
97
97
  globalThis.expect(Stdlib_Result.isOk(await LocalAuth$ReventlessLocal.Login.issue("later@example.com", "", undefined))).toBe(true);
98
98
  });
99
99
  globalThis.test("a push address is refused, since nothing signs in with one", async () => {
100
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(freshFile(), undefined);
100
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
101
101
  let answer = await provider.createPrincipal({
102
102
  TAG: "ToPush",
103
103
  _0: {
@@ -117,7 +117,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
117
117
  globalThis.test("groups round-trip through the file", async () => {
118
118
  resetAll();
119
119
  let file = freshFile();
120
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(file, undefined);
120
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
121
121
  let principal = {
122
122
  providerId: "grp@example.com"
123
123
  };
@@ -150,7 +150,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
150
150
  });
151
151
  globalThis.test("grouping a principal that does not exist is refused", async () => {
152
152
  resetAll();
153
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(freshFile(), undefined);
153
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
154
154
  let answer = await provider.addToGroup({
155
155
  providerId: "ghost"
156
156
  }, "Shopper");
@@ -165,7 +165,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
165
165
  globalThis.test("deleting removes the account from the file and the login, twice", async () => {
166
166
  resetAll();
167
167
  let file = freshFile();
168
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(file, undefined);
168
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
169
169
  let principal = {
170
170
  providerId: "gone@example.com"
171
171
  };
@@ -198,7 +198,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
198
198
  password: pw
199
199
  groups: [Admin]
200
200
  `, "utf8");
201
- let provider = Capability_IdentityProvider_UserStore$ReventlessLocal.make(file, undefined);
201
+ let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
202
202
  await provider.createPrincipal({
203
203
  TAG: "ToEmail",
204
204
  _0: "c@example.com"
@@ -22,14 +22,14 @@ describe("LocalCapabilities.unmetWarning", () => {
22
22
  let message = LocalCapabilities.unmetWarning(~declared)->Option.getOr("")
23
23
  expect(message->String.includes("Geocoding (GeocodeAddress)"))->toBe(true)
24
24
  expect(message->String.includes("Capability_Messaging_Log.make()"))->toBe(true)
25
- expect(message->String.includes("Capability_IdentityProvider_UserStore.make()"))->toBe(true)
25
+ expect(message->String.includes("Capability_IdentityProvider_AccountStore.make()"))->toBe(true)
26
26
  })
27
27
 
28
28
  testSync("a registered provider leaves the warning", () => {
29
29
  reset()
30
30
  LocalCapabilities.registerMessagingSender("Shop <shop@example.test>")
31
31
  LocalCapabilities.registerIdentityProvider(
32
- Capability_IdentityProvider_UserStore.make(~path="/nonexistent/users.yaml", ()),
32
+ Capability_IdentityProvider_AccountStore.make(~path="/nonexistent/accounts.yaml", ()),
33
33
  )
34
34
  expect(LocalCapabilities.provisioned())->toEqual([
35
35
  Reventless.CapabilityNeed.Messaging,
@@ -3,7 +3,7 @@
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
  import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
5
5
  import * as LocalCapabilities$ReventlessLocal from "../../src/adapter/LocalCapabilities.res.mjs";
6
- import * as Capability_IdentityProvider_UserStore$ReventlessLocal from "../../src/capability/Capability_IdentityProvider_UserStore.res.mjs";
6
+ import * as Capability_IdentityProvider_AccountStore$ReventlessLocal from "../../src/capability/Capability_IdentityProvider_AccountStore.res.mjs";
7
7
 
8
8
  let declared = [
9
9
  [
@@ -32,12 +32,12 @@ globalThis.describe("LocalCapabilities.unmetWarning", () => {
32
32
  let message = Stdlib_Option.getOr(LocalCapabilities$ReventlessLocal.unmetWarning(declared), "");
33
33
  globalThis.expect(message.includes("Geocoding (GeocodeAddress)")).toBe(true);
34
34
  globalThis.expect(message.includes("Capability_Messaging_Log.make()")).toBe(true);
35
- globalThis.expect(message.includes("Capability_IdentityProvider_UserStore.make()")).toBe(true);
35
+ globalThis.expect(message.includes("Capability_IdentityProvider_AccountStore.make()")).toBe(true);
36
36
  });
37
37
  globalThis.test("a registered provider leaves the warning", () => {
38
38
  reset();
39
39
  LocalCapabilities$ReventlessLocal.registerMessagingSender("Shop <shop@example.test>");
40
- LocalCapabilities$ReventlessLocal.registerIdentityProvider(Capability_IdentityProvider_UserStore$ReventlessLocal.make("/nonexistent/users.yaml", undefined));
40
+ LocalCapabilities$ReventlessLocal.registerIdentityProvider(Capability_IdentityProvider_AccountStore$ReventlessLocal.make("/nonexistent/accounts.yaml", undefined));
41
41
  globalThis.expect(LocalCapabilities$ReventlessLocal.provisioned()).toEqual([
42
42
  "Messaging",
43
43
  "IdentityProvider",
@@ -0,0 +1,291 @@
1
+ // Another plugin's fact arrives through an Extension and a slice, and delivery is
2
+ // at-least-once. The slice here has no check of its own — every RecordDemand
3
+ // appends — so what keeps a redelivered ItemOrdered from counting twice is the
4
+ // framework: the Extension's command carries a stable id, and the handler drops a
5
+ // command the partition already holds events for. On both local backends, and
6
+ // for two concurrent copies.
7
+
8
+ open JestGlobals
9
+
10
+ let _ = TestRunner.setup()
11
+ let opts: Pulumi.CustomResourceOptions.t = {}
12
+
13
+ module Message = ReventlessCore.Message
14
+
15
+ // ─── The provider's extension point and the consumer's slice ────────────────
16
+
17
+ module Port = {
18
+ let name = "Ordering.Orders"
19
+
20
+ @schema
21
+ type command = Noop
22
+
23
+ @schema
24
+ type event = ItemOrdered({productId: string, orderId: string})
25
+
26
+ @schema
27
+ type directive = NoDirective
28
+
29
+ let moduleUrl: string = %raw(`import.meta.url`)
30
+ }
31
+
32
+ module DemandSlice = {
33
+ let name = "RecordDemandNoCheck"
34
+ module Id = Reventless.Id.String
35
+ let moduleUrl: string = %raw(`import.meta.url`)
36
+
37
+ // The produced event joins a product and an order, so the partition is
38
+ // declared, as the shop's RecordProductDemand does.
39
+ @schema
40
+ type event =
41
+ DemandRecorded({productId: @s.matches(Reventless.DcbTag.partition) string, orderId: string})
42
+
43
+ @schema
44
+ type consumedEvent =
45
+ DemandRecorded({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
46
+
47
+ @schema
48
+ type command =
49
+ RecordDemand({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
50
+
51
+ @schema
52
+ type error = Never
53
+
54
+ type role = Reventless.Role.name
55
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAuthenticated
56
+ type lifecycleState = unit
57
+ let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
58
+ let traits: array<Reventless.Trait.t> = []
59
+ }
60
+
61
+ module DemandDecision = {
62
+ type state = {orders: array<string>}
63
+ let initialState = {orders: []}
64
+ let moduleUrl = DemandSlice.moduleUrl
65
+
66
+ let evolve = (state, event: DemandSlice.consumedEvent) =>
67
+ switch event {
68
+ | DemandRecorded({orderId}) => {orders: state.orders->Array.concat([orderId])}
69
+ }
70
+
71
+ // Deliberately no "already counted" rule.
72
+ let decide = (_state, command: DemandSlice.command): result<
73
+ array<DemandSlice.event>,
74
+ DemandSlice.error,
75
+ > =>
76
+ switch command {
77
+ | RecordDemand({productId, orderId}) => Ok([DemandRecorded({productId, orderId})])
78
+ }
79
+ }
80
+
81
+ module DemandHandler = ReventlessCore.StateChangeSlice_Callback.Make(DemandSlice, DemandDecision)
82
+
83
+ let partitionTag = Reventless.DcbTag.Simple({key: "productId"})
84
+
85
+ // The consumer's Extension, as the catalog's Orders_Extension is written.
86
+ module DemandExtension = ReventlessInfra.ExtensionMapping.Make({
87
+ module ExtensionPoint = Port
88
+ module Delegate = DemandSlice
89
+ let moduleUrl: string = %raw(`import.meta.url`)
90
+ let delegateModuleUrl = DemandSlice.moduleUrl
91
+ let mapIncomingEvent = (_id, event: Port.event, _meta, _pluginDef, _queryEngine) =>
92
+ switch event {
93
+ | ItemOrdered({productId, orderId}) => [
94
+ ReventlessInfra.ExtensionMapping.PublishStateChangeSliceCommand(
95
+ DemandSlice.RecordDemand({productId, orderId}),
96
+ ),
97
+ ]
98
+ }
99
+ let mapOutgoingEvent = None
100
+ let handledEvents = []
101
+ let issuedCommands = []
102
+ })
103
+
104
+ let queryEngine: Reventless.QueryEngine.operations = {
105
+ scan: async (~readModelName as _, ~filterConfigs as _, ~limit as _) => [],
106
+ query: async (
107
+ ~readModelName as _,
108
+ ~key as _=?,
109
+ ~id as _,
110
+ ~subIdConfig as _=?,
111
+ ~filterConfigs as _=?,
112
+ ~ascending as _=?,
113
+ ~limit as _=?,
114
+ ) => [],
115
+ }
116
+
117
+ let pluginDefinition: Reventless.Plugin.pluginDefinition = {
118
+ id: "Catalog@1",
119
+ name: "Catalog",
120
+ version: "1",
121
+ extensionPoints: [],
122
+ extensions: [{name: "Catalog", extensionPointName: Port.name}],
123
+ eventCollector: "eventCollector",
124
+ extensionProtocols: [],
125
+ apiSchemaFragment: None,
126
+ structure: None,
127
+ kind: Domain,
128
+ }
129
+
130
+ // One published ItemOrdered, as the extension point stored and published it;
131
+ // every delivery of it carries this same envelope.
132
+ let published: Message.event'<string, Port.event> = {
133
+ id: "order-1",
134
+ meta: {
135
+ service: Port.name,
136
+ time: "2024-01-01T00:00:00Z",
137
+ msgId: "item-ordered-msg",
138
+ correlationId: "place-order-cmd",
139
+ causationId: "order-placed-event",
140
+ },
141
+ event: ItemOrdered({productId: "product-1", orderId: "order-1"}),
142
+ }
143
+
144
+ // What the Extension sends for one delivery of `published`.
145
+ let commandOf = (event': Message.event'<string, Port.event>) =>
146
+ switch DemandExtension.mapIncomingEvent(event', pluginDefinition, queryEngine) {
147
+ | [ReventlessInfra.ExtensionMapping.AbstractPublishAggregateCommand(_, cmdJson)] => cmdJson
148
+ | _ => JsError.throwWithMessage("expected one slice command")
149
+ }
150
+
151
+ // ─── Backends ───────────────────────────────────────────────────────────────
152
+
153
+ let eventLogOn = async (~name) => {
154
+ module Bus = LocalBus.Make()
155
+ module Storage = LocalDcbEventLogStorage.Make(Bus)
156
+ let s = Storage.make(~name, ~indexes=[], ~partitionTag, ~opts)
157
+ let storage = await s.operations->TestRunner.resolve
158
+ module Log = ReventlessCore.DcbEventLog_Operations.Make({
159
+ let name = name
160
+ let serviceName = name
161
+ let storage = storage
162
+ let publishJson = async (_, _, _) => ()
163
+ })
164
+ let eventLog: ReventlessCore.DcbEventLog.operations = {
165
+ read: Log.read,
166
+ append: Log.append,
167
+ readStream: Log.readStream,
168
+ appendStream: Log.appendStream,
169
+ }
170
+ eventLog
171
+ }
172
+
173
+ let runUnderMemory = async fn => {
174
+ BackendState.setMemory()
175
+ await fn()
176
+ }
177
+
178
+ let runUnderSqlite = async fn => {
179
+ let db = SqliteDriver.openDb(~path=":memory:")
180
+ BackendState.setSqlite(~db, ~path=":memory:")
181
+ await fn()
182
+ BackendState.setMemory()
183
+ db->SqliteDriver.close
184
+ }
185
+
186
+ let deliver = (eventLog, cmd: Message.commandJson) =>
187
+ DemandHandler.handleCommands(
188
+ ~partitionTag,
189
+ eventLog,
190
+ Stream.fromIterable([
191
+ {
192
+ ReventlessInfra.CommandTopic.command: cmd
193
+ ->ReventlessCore.CommandTopic_Helpers.encodeCommandJson
194
+ ->Message.decodeCommand'(Reventless.Id.String.schema, DemandSlice.commandSchema),
195
+ reference: cmd.meta.msgId,
196
+ },
197
+ ]),
198
+ )->Effect.runPromise
199
+
200
+ let recordedDemands = async (eventLog: ReventlessCore.DcbEventLog.operations) =>
201
+ (await eventLog.read(~query=[{eventTypes: ["DemandRecorded"]}])).events
202
+
203
+ let counter = ref(0)
204
+ let fresh = prefix => {
205
+ counter := counter.contents + 1
206
+ `${prefix}-${counter.contents->Int.toString}`
207
+ }
208
+
209
+ let _ = beforeEach(() => DemandHandler.resetCache())
210
+
211
+ let backends = [("in-memory", runUnderMemory), ("SQLite", runUnderSqlite)]
212
+
213
+ backends->Array.forEach(((label, runUnder)) =>
214
+ describe(`a redelivered fact is recorded once (${label} backend)`, () => {
215
+ testPromise(
216
+ "the same published event delivered twice appends one event",
217
+ async () =>
218
+ await runUnder(
219
+ async () => {
220
+ let log = await eventLogOn(~name=fresh("RepeatOnce"))
221
+ let first = await deliver(log, commandOf(published))
222
+ let again = await deliver(log, commandOf(published))
223
+ expect(first->Array.length)->toBe(1)
224
+ expect(again)->toEqual(first) // accepted both times
225
+ let events = await recordedDemands(log)
226
+ expect(events->Array.length)->toBe(1)
227
+ // The command is the stored event's cause, and the published event the command's.
228
+ let cmd = commandOf(published)
229
+ expect((events->Array.getUnsafe(0)).meta.causationId)->toEqual(Some(cmd.meta.msgId))
230
+ expect(cmd.meta.causationId)->toEqual(Some(published.meta.msgId))
231
+ },
232
+ ),
233
+ )
234
+
235
+ testPromise(
236
+ "a different order for the same product still counts",
237
+ async () =>
238
+ await runUnder(
239
+ async () => {
240
+ let log = await eventLogOn(~name=fresh("RepeatOther"))
241
+ let _ = await deliver(log, commandOf(published))
242
+ let second: Message.event'<string, Port.event> = {
243
+ id: "order-2",
244
+ meta: {...published.meta, msgId: "item-ordered-msg-2"},
245
+ event: Port.ItemOrdered({productId: "product-1", orderId: "order-2"}),
246
+ }
247
+ let _ = await deliver(log, commandOf(second))
248
+ let _ = await deliver(log, commandOf(published))
249
+ let _ = await deliver(log, commandOf(second))
250
+ expect((await recordedDemands(log))->Array.length)->toBe(2)
251
+ },
252
+ ),
253
+ )
254
+
255
+ testPromise(
256
+ "a command with a random id is decided every time",
257
+ async () =>
258
+ await runUnder(
259
+ async () => {
260
+ let log = await eventLogOn(~name=fresh("RepeatRandom"))
261
+ let cmd = commandOf(published)
262
+ // The envelope of a command nobody derived: fresh id, no mark.
263
+ let plain = {...cmd, meta: Message.deriveMeta(~parent=cmd.meta)}
264
+ let _ = await deliver(log, plain)
265
+ let _ = await deliver(log, plain)
266
+ expect((await recordedDemands(log))->Array.length)->toBe(2)
267
+ },
268
+ ),
269
+ )
270
+
271
+ testPromise(
272
+ "two concurrent copies append one event",
273
+ async () =>
274
+ await runUnder(
275
+ async () => {
276
+ let log = await eventLogOn(~name=fresh("RepeatConcurrent"))
277
+ let cmd = commandOf(published)
278
+ // Both read an empty partition; the second append conflicts, re-reads,
279
+ // and finds the first one's event.
280
+ let (a, b) = await Promise.all2((deliver(log, cmd), deliver(log, cmd)))
281
+ expect(a->Array.length)->toBe(1)
282
+ expect(b->Array.length)->toBe(1)
283
+ expect(a->Array.getUnsafe(0)->Result.isOk && b->Array.getUnsafe(0)->Result.isOk)->toBe(
284
+ true,
285
+ )
286
+ expect((await recordedDemands(log))->Array.length)->toBe(1)
287
+ },
288
+ ),
289
+ )
290
+ })
291
+ )