@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.
- package/CHANGELOG.md +30 -0
- package/package.json +11 -11
- package/src/EmitCapabilities.res +12 -5
- package/src/EmitCapabilities.res.mjs +13 -1
- package/src/Platform.res +6 -7
- package/src/Platform.res.mjs +10 -12
- package/src/adapter/Auth/{UserStore.res → AccountStore.res} +17 -17
- package/src/adapter/Auth/{UserStore.res.mjs → AccountStore.res.mjs} +11 -11
- package/src/adapter/Auth/LocalAuth.res +5 -5
- package/src/adapter/LocalCapabilities.res +2 -2
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -1
- package/src/capability/{Capability_IdentityProvider_UserStore.res → Capability_IdentityProvider_AccountStore.res} +1 -1
- package/tests/EmitCapabilitiesTest.res +6 -0
- package/tests/EmitCapabilitiesTest.res.mjs +6 -0
- package/tests/adapter/{LocalAuthUserStoreTest.res → LocalAuthAccountStoreTest.res} +48 -48
- package/tests/adapter/{LocalAuthUserStoreTest.res.mjs → LocalAuthAccountStoreTest.res.mjs} +44 -44
- package/tests/capability/{Capability_IdentityProvider_UserStoreTest.res → Capability_IdentityProvider_AccountStoreTest.res} +15 -13
- package/tests/capability/{Capability_IdentityProvider_UserStoreTest.res.mjs → Capability_IdentityProvider_AccountStoreTest.res.mjs} +15 -15
- package/tests/capability/LocalCapabilitiesTest.res +2 -2
- package/tests/capability/LocalCapabilitiesTest.res.mjs +3 -3
- package/tests/components/dcb/DcbRepeatTest.res +291 -0
- package/tests/components/dcb/DcbRepeatTest.res.mjs +420 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +71 -15
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +119 -14
- package/tests/plugin/Plugin_Fixtures.res +1 -3
- package/tests/plugin/Plugin_Fixtures.res.mjs +1 -2
- /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
|
|
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", "
|
|
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
|
-
|
|
29
|
+
AccountStore$ReventlessLocal.resetResolution();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function restartFrom(file) {
|
|
33
33
|
resetAll();
|
|
34
|
-
let message =
|
|
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("
|
|
42
|
+
globalThis.describe("Capability_IdentityProvider_AccountStore", () => {
|
|
43
43
|
globalThis.test("publishes all four operations", () => {
|
|
44
|
-
globalThis.expect(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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("
|
|
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
|
-
|
|
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
|
|
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("
|
|
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(
|
|
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
|
+
)
|