@reventlessdev/reventless-aws 3.0.0-alpha.181 → 3.0.0-alpha.183
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 +14 -0
- package/package.json +6 -6
- package/src/Platform.res +71 -34
- package/src/Platform.res.mjs +58 -70
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +74 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +61 -12
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +140 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +158 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +164 -20
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +149 -2
- package/tests/integration/DcbIntegrationHarness.res +19 -8
- package/tests/integration/DcbIntegrationHarness.res.mjs +5 -6
|
@@ -129,18 +129,20 @@ globalThis.describe("DCB DynamoDb integration — fence-scope = read-scope (Issu
|
|
|
129
129
|
});
|
|
130
130
|
let query = [
|
|
131
131
|
{
|
|
132
|
+
eventTypes: ["OrderPlaced"],
|
|
132
133
|
tags: [{
|
|
133
134
|
key: "orderId",
|
|
134
135
|
value: "O1"
|
|
135
136
|
}]
|
|
136
137
|
},
|
|
137
138
|
{
|
|
139
|
+
eventTypes: ["CatalogProductSynced"],
|
|
138
140
|
tags: [productTag]
|
|
139
141
|
}
|
|
140
142
|
];
|
|
141
143
|
let after = Stdlib_Option.getOr(await readAfter(table, query), "");
|
|
142
144
|
globalThis.expect(after === "").toBe(false);
|
|
143
|
-
await DcbIntegrationHarness$ReventlessAws.setFence(table, productTag, after + "z");
|
|
145
|
+
await DcbIntegrationHarness$ReventlessAws.setFence(table, productTag, ["CatalogProductSynced"], after + "z");
|
|
144
146
|
let r = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("OrderPlaced", [
|
|
145
147
|
{
|
|
146
148
|
key: "orderId",
|
|
@@ -384,6 +386,7 @@ globalThis.describe("DCB DynamoDb integration — optimistic concurrency primiti
|
|
|
384
386
|
}
|
|
385
387
|
];
|
|
386
388
|
let query = [{
|
|
389
|
+
eventTypes: ["ProductDemandRecorded"],
|
|
387
390
|
tags: tags
|
|
388
391
|
}];
|
|
389
392
|
let seedRes = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("ProductDemandRecorded", tags)], {
|
|
@@ -400,7 +403,7 @@ globalThis.describe("DCB DynamoDb integration — optimistic concurrency primiti
|
|
|
400
403
|
await DcbIntegrationHarness$ReventlessAws.setFence(table, {
|
|
401
404
|
key: "productId",
|
|
402
405
|
value: "p1"
|
|
403
|
-
}, after + "z");
|
|
406
|
+
}, ["ProductDemandRecorded"], after + "z");
|
|
404
407
|
let r = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("ProductDemandRecorded", tags)], {
|
|
405
408
|
query: query,
|
|
406
409
|
after: after
|
|
@@ -416,6 +419,150 @@ globalThis.describe("DCB DynamoDb integration — optimistic concurrency primiti
|
|
|
416
419
|
});
|
|
417
420
|
});
|
|
418
421
|
|
|
422
|
+
globalThis.describe("DCB DynamoDb integration — composite partition hot-fence fix", () => {
|
|
423
|
+
let specKeys = [
|
|
424
|
+
"environment",
|
|
425
|
+
"platformName",
|
|
426
|
+
"pluginName",
|
|
427
|
+
"resourceName"
|
|
428
|
+
];
|
|
429
|
+
let specSeps = [
|
|
430
|
+
"/",
|
|
431
|
+
"/",
|
|
432
|
+
"/"
|
|
433
|
+
];
|
|
434
|
+
let pt = {
|
|
435
|
+
TAG: "Composite",
|
|
436
|
+
_0: {
|
|
437
|
+
keys: specKeys,
|
|
438
|
+
seps: specSeps
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
let members = resourceName => [
|
|
442
|
+
{
|
|
443
|
+
key: "environment",
|
|
444
|
+
value: "prod"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
key: "platformName",
|
|
448
|
+
value: "plat"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
key: "pluginName",
|
|
452
|
+
value: "plug"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
key: "resourceName",
|
|
456
|
+
value: resourceName
|
|
457
|
+
}
|
|
458
|
+
];
|
|
459
|
+
globalThis.test("distinct composite entities sharing a prefix do NOT false-conflict", async () => {
|
|
460
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
461
|
+
let tagsA = members("resA");
|
|
462
|
+
let tagsB = members("resB");
|
|
463
|
+
let rA = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("ResourceAdded", tagsA)], {
|
|
464
|
+
query: [{
|
|
465
|
+
tags: tagsA
|
|
466
|
+
}]
|
|
467
|
+
}, pt, undefined);
|
|
468
|
+
let rB = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("ResourceAdded", tagsB)], {
|
|
469
|
+
query: [{
|
|
470
|
+
tags: tagsB
|
|
471
|
+
}]
|
|
472
|
+
}, pt, undefined);
|
|
473
|
+
globalThis.expect(isOk(rA)).toBe(true);
|
|
474
|
+
globalThis.expect(isOk(rB)).toBe(true);
|
|
475
|
+
});
|
|
476
|
+
globalThis.test("two first-writers of the SAME composite key still serialize (OCC preserved)", async () => {
|
|
477
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
478
|
+
let tags = members("resA");
|
|
479
|
+
let query = [{
|
|
480
|
+
tags: tags
|
|
481
|
+
}];
|
|
482
|
+
let r1 = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("ResourceAdded", tags)], {
|
|
483
|
+
query: query
|
|
484
|
+
}, pt, undefined);
|
|
485
|
+
let r2 = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("ResourceAdded", tags)], {
|
|
486
|
+
query: query
|
|
487
|
+
}, pt, undefined);
|
|
488
|
+
globalThis.expect(isOk(r1)).toBe(true);
|
|
489
|
+
globalThis.expect(isConflict(r2)).toBe(true);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
globalThis.describe("DCB DynamoDb integration — per-type fence granularity", () => {
|
|
494
|
+
let productTag = {
|
|
495
|
+
key: "productId",
|
|
496
|
+
value: "P"
|
|
497
|
+
};
|
|
498
|
+
let change = async (table, eventType) => {
|
|
499
|
+
let query = [{
|
|
500
|
+
eventTypes: [eventType],
|
|
501
|
+
tags: [productTag]
|
|
502
|
+
}];
|
|
503
|
+
let after = await readAfter(table, query);
|
|
504
|
+
return await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event(eventType, [productTag])], {
|
|
505
|
+
query: query,
|
|
506
|
+
after: after
|
|
507
|
+
}, {
|
|
508
|
+
TAG: "Simple",
|
|
509
|
+
_0: {
|
|
510
|
+
key: "productId"
|
|
511
|
+
}
|
|
512
|
+
}, undefined);
|
|
513
|
+
};
|
|
514
|
+
globalThis.test("interleaved distinct-type changes on one product all succeed (never wedges)", async () => {
|
|
515
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
516
|
+
await seed(table, event("ProductAdded", [productTag]), {
|
|
517
|
+
TAG: "Simple",
|
|
518
|
+
_0: {
|
|
519
|
+
key: "productId"
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
let results = [
|
|
523
|
+
await change(table, "PriceChanged"),
|
|
524
|
+
await change(table, "NameChanged"),
|
|
525
|
+
await change(table, "DescriptionChanged"),
|
|
526
|
+
await change(table, "PriceChanged"),
|
|
527
|
+
await change(table, "NameChanged")
|
|
528
|
+
];
|
|
529
|
+
globalThis.expect(results.every(isOk)).toBe(true);
|
|
530
|
+
});
|
|
531
|
+
globalThis.test("two concurrent SAME-type changes still serialize (per-type OCC preserved)", async () => {
|
|
532
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
533
|
+
await seed(table, event("ProductAdded", [productTag]), {
|
|
534
|
+
TAG: "Simple",
|
|
535
|
+
_0: {
|
|
536
|
+
key: "productId"
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
await change(table, "NameChanged");
|
|
540
|
+
let query = [{
|
|
541
|
+
eventTypes: ["NameChanged"],
|
|
542
|
+
tags: [productTag]
|
|
543
|
+
}];
|
|
544
|
+
let after = await readAfter(table, query);
|
|
545
|
+
let rename = () => DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("NameChanged", [productTag])], {
|
|
546
|
+
query: query,
|
|
547
|
+
after: after
|
|
548
|
+
}, {
|
|
549
|
+
TAG: "Simple",
|
|
550
|
+
_0: {
|
|
551
|
+
key: "productId"
|
|
552
|
+
}
|
|
553
|
+
}, undefined);
|
|
554
|
+
let results = await Promise.all([
|
|
555
|
+
rename(),
|
|
556
|
+
rename()
|
|
557
|
+
]);
|
|
558
|
+
let oks = results.filter(isOk).length;
|
|
559
|
+
let conflicts = results.filter(isConflict).length;
|
|
560
|
+
globalThis.expect(oks <= 1).toBe(true);
|
|
561
|
+
globalThis.expect(oks + conflicts | 0).toBe(2);
|
|
562
|
+
globalThis.expect(conflicts >= 1).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
419
566
|
let H;
|
|
420
567
|
|
|
421
568
|
let Runtime;
|
|
@@ -85,18 +85,29 @@ let freshTable = async () => {
|
|
|
85
85
|
await createDcbTable(`DcbItTest_${counter.contents->Int.toString}`)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Directly
|
|
89
|
-
//
|
|
88
|
+
// Directly advance a fence sentinel — used to simulate a concurrent writer having
|
|
89
|
+
// bumped a fence between a slice's read and append. The fence model is per event
|
|
90
|
+
// type (`pos#<eventType>` attributes, not a scalar `lastPosition`), so the caller
|
|
91
|
+
// names the event type(s) whose position to set; each `pos#<eventType>` is written
|
|
92
|
+
// to `position`, exactly as a real conditional append would advance them.
|
|
90
93
|
let setFence = async (
|
|
91
94
|
table: Util_DynamoDb_Runtime.resolvedTable,
|
|
92
95
|
tag: Reventless.DcbTag.tag,
|
|
93
|
-
~
|
|
96
|
+
~eventTypes: array<string>,
|
|
97
|
+
~position: string,
|
|
94
98
|
) => {
|
|
95
99
|
let item =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
Array.concat(
|
|
101
|
+
[
|
|
102
|
+
("id", s(DcbEventLogStorage_DynamoDb_Runtime.fencePartitionKey(tag))),
|
|
103
|
+
("position", s("FENCE")),
|
|
104
|
+
],
|
|
105
|
+
eventTypes->Array.map(et => (
|
|
106
|
+
DcbEventLogStorage_DynamoDb_Runtime.fenceTypeAttr(et),
|
|
107
|
+
s(position),
|
|
108
|
+
)),
|
|
109
|
+
)
|
|
110
|
+
->Dict.fromArray
|
|
111
|
+
->JSON.Encode.object
|
|
101
112
|
await Util_DynamoDb_Runtime.put(table, item)
|
|
102
113
|
}
|
|
@@ -129,7 +129,7 @@ async function freshTable() {
|
|
|
129
129
|
return await createDcbTable(`DcbItTest_` + counter.contents.toString());
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
async function setFence(table, tag,
|
|
132
|
+
async function setFence(table, tag, eventTypes, position) {
|
|
133
133
|
let item = Object.fromEntries([
|
|
134
134
|
[
|
|
135
135
|
"id",
|
|
@@ -138,12 +138,11 @@ async function setFence(table, tag, lastPosition) {
|
|
|
138
138
|
[
|
|
139
139
|
"position",
|
|
140
140
|
"FENCE"
|
|
141
|
-
],
|
|
142
|
-
[
|
|
143
|
-
"lastPosition",
|
|
144
|
-
lastPosition
|
|
145
141
|
]
|
|
146
|
-
]
|
|
142
|
+
].concat(eventTypes.map(et => [
|
|
143
|
+
DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.fenceTypeAttr(et),
|
|
144
|
+
position
|
|
145
|
+
])));
|
|
147
146
|
return await Util_DynamoDb_Runtime$ReventlessAws.put(table, item);
|
|
148
147
|
}
|
|
149
148
|
|