@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.
@@ -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 overwrite a fence sentinel's `lastPosition` — used to simulate a
89
- // concurrent writer having advanced a fence between a slice's read and append.
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
- ~lastPosition: string,
96
+ ~eventTypes: array<string>,
97
+ ~position: string,
94
98
  ) => {
95
99
  let item =
96
- Dict.fromArray([
97
- ("id", s(DcbEventLogStorage_DynamoDb_Runtime.fencePartitionKey(tag))),
98
- ("position", s("FENCE")),
99
- ("lastPosition", s(lastPosition)),
100
- ])->JSON.Encode.object
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, lastPosition) {
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