@reventlessdev/reventless-aws 3.0.0-alpha.343 → 3.0.0-alpha.345

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 (234) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +11 -11
  3. package/scripts/ProvisionIdentity.res +8 -3
  4. package/src/Platform.res +327 -328
  5. package/src/Platform.res.mjs +4 -2
  6. package/src/Platform_Casts.res +13 -4
  7. package/src/adapter/Api/AppSyncEventsSigner_Ops.res +8 -4
  8. package/src/adapter/Api/AppSync_EventsApi.res +12 -14
  9. package/src/adapter/Api/AppSync_Resolver_Native.res +3 -4
  10. package/src/adapter/Api/AppSync_Resolver_Retrying.res +78 -70
  11. package/src/adapter/Api/AppSync_SourceApiAssociation_Retrying.res +11 -8
  12. package/src/adapter/Api/Platform_AdminScan_Ops.res +15 -14
  13. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +10 -2
  14. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +40 -50
  15. package/src/adapter/Api/Platform_UIFragments_Lambda.res +39 -32
  16. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +5 -2
  17. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +0 -1
  18. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -2
  19. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res +1 -4
  20. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +5 -2
  21. package/src/adapter/Auth/Auth_Cognito.res +3 -9
  22. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +1 -4
  23. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_NoOp.res +3 -1
  24. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res +1 -4
  25. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +7 -4
  26. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +6 -1
  27. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +7 -4
  28. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res +24 -18
  29. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +9 -4
  30. package/src/adapter/CommandTopic/CommandTopicRegistry.res +1 -4
  31. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +15 -15
  32. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res +9 -5
  33. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +179 -213
  34. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +5 -5
  35. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +6 -2
  36. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +14 -7
  37. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +14 -7
  38. package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res +6 -3
  39. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res +6 -1
  40. package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res +13 -17
  41. package/src/adapter/EventLog/EventLogStorage_Postgres.res +7 -4
  42. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +12 -7
  43. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +4 -2
  44. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res +3 -2
  45. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res +20 -12
  46. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res +21 -9
  47. package/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res +5 -3
  48. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +6 -3
  49. package/src/adapter/Mcp/MCP_Lambda.res +31 -46
  50. package/src/adapter/Messaging/Messaging_Ses_Backend.res +11 -7
  51. package/src/adapter/Messaging/Messaging_Ses_Backend.res.mjs +2 -4
  52. package/src/adapter/Monitoring/Monitoring_CloudWatch.res +16 -16
  53. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +5 -10
  54. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +4 -2
  55. package/src/adapter/Postgres/PgConnection.res +6 -5
  56. package/src/adapter/Postgres/PgMigration_Builder.res +4 -8
  57. package/src/adapter/Postgres/PgRuntime.res +4 -7
  58. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +9 -11
  59. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +22 -21
  60. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +114 -111
  61. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +1 -7
  62. package/src/adapter/QueryDb/QueryDbStorage.res +4 -2
  63. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +23 -24
  64. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res +8 -2
  65. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res +71 -73
  66. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -2
  67. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res +1 -4
  68. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +8 -2
  69. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +73 -81
  70. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res +72 -78
  71. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res +44 -67
  72. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res +44 -67
  73. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +53 -82
  74. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +47 -77
  75. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +18 -11
  76. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +80 -97
  77. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +2 -7
  78. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +16 -10
  79. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +24 -15
  80. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +17 -37
  81. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +31 -57
  82. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +18 -24
  83. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +1 -5
  84. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +4 -3
  85. package/src/adapter/Runtime/HeartbeatEntryPoint.res +0 -1
  86. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +4 -2
  87. package/src/adapter/Runtime/PgMigrationEntryPoint.res +0 -1
  88. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +3 -2
  89. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +1 -2
  90. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +4 -5
  91. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +22 -10
  92. package/src/adapter/Runtime/RuntimeExtensionEntryPoint_Ops.res +8 -2
  93. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +14 -31
  94. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +16 -17
  95. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +10 -9
  96. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +46 -52
  97. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +36 -41
  98. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +31 -37
  99. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res +6 -7
  100. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res +4 -6
  101. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res +2 -6
  102. package/src/adapter/StateTopic/StateTopic_AppSync.res +21 -16
  103. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +36 -32
  104. package/src/adapter/Task/TaskBucket_S3.res +6 -1
  105. package/src/adapter/Upload/Upload_Claim_S3.res +17 -20
  106. package/src/adapter/Upload/Upload_Claim_S3_Ops.res +5 -2
  107. package/src/adapter/Upload/Upload_Presign_S3.res +51 -49
  108. package/src/capability/Capability_Geocoding_AwsLocation.res +4 -1
  109. package/src/capability/Capability_Messaging.res +4 -5
  110. package/src/capability/Capability_Messaging_Ses.res +3 -1
  111. package/src/capability/Capability_ObjectStore_S3.res +1 -7
  112. package/src/components/Api/AppSync_Adapter.res +28 -41
  113. package/src/components/Api/AppSync_SdlDecorate.res +38 -30
  114. package/src/components/AutomationSlice_Builder.res +6 -4
  115. package/src/components/ExtensionPoint_Builder.res +9 -10
  116. package/src/components/InboundTranslationSlice_Builder.res +7 -8
  117. package/src/components/OutboundTranslationSlice_Builder.res +14 -8
  118. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  119. package/src/components/SideEffectHandler_Single.res +4 -4
  120. package/src/components/StateViewSlice_Builder.res +6 -4
  121. package/src/components/StateViewSlice_Builder_Stream.res +6 -4
  122. package/src/components/Task_Builder_PerBucket.res +10 -11
  123. package/src/plugin/cloner/ClonerRunner_Fargate.res +3 -10
  124. package/src/plugin/cloner/ClonerRunner_Fargate_Runtime.res +1 -4
  125. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +5 -5
  126. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +19 -21
  127. package/src/plugin/runtime/PluginRuntime_Builder.res +167 -188
  128. package/src/plugin/runtime/Util_PluginMessage_Runtime.res +6 -5
  129. package/src/plugin/stack/Plugin_Stack.res +104 -103
  130. package/src/util/Util_AlarmSpec.res +2 -1
  131. package/src/util/Util_AppSync.res +8 -2
  132. package/src/util/Util_AppSync_Caller.res +15 -14
  133. package/src/util/Util_Bundle.res +144 -67
  134. package/src/util/Util_Bundle.res.mjs +64 -13
  135. package/src/util/Util_CognitoGroupUser_Runtime.res +32 -34
  136. package/src/util/Util_Cognito_Runtime.res +11 -7
  137. package/src/util/Util_DeadLetterQueue.res +12 -2
  138. package/src/util/Util_DynamoDb.res +5 -3
  139. package/src/util/Util_DynamoDbStream.res +7 -3
  140. package/src/util/Util_DynamoDbStream_Runtime.res +1 -2
  141. package/src/util/Util_DynamoDb_Runtime.res +23 -41
  142. package/src/util/Util_HostUiDomain.res +6 -1
  143. package/src/util/Util_Kinesis_Runtime.res +10 -11
  144. package/src/util/Util_Lambda.res +12 -5
  145. package/src/util/Util_LambdaEnvBudget.res +5 -5
  146. package/src/util/Util_LambdaLogging.res +1 -2
  147. package/src/util/Util_LogGroup_Adopting.res +1 -4
  148. package/src/util/Util_ResourceNaming.res +1 -2
  149. package/src/util/Util_SNS.res +8 -3
  150. package/src/util/Util_SNS_FIFO.res +1 -1
  151. package/src/util/Util_SNS_Runtime.res +11 -10
  152. package/src/util/Util_SQS.res +4 -2
  153. package/src/util/Util_SQS_FIFO.res +5 -2
  154. package/src/util/Util_SQS_Runtime.res +37 -36
  155. package/src/util/Util_ShellConfig.res +3 -2
  156. package/src/util/Util_StaticBundle.res +1 -3
  157. package/src/util/Util_StoreLayout.res +4 -1
  158. package/tests/AWS_TagsTest.res +16 -5
  159. package/tests/AppSyncEventsSigner_OpsTest.res +11 -2
  160. package/tests/AppSync_AdapterTest.res +203 -224
  161. package/tests/AppSync_Resolver_RetryingTest.res +64 -44
  162. package/tests/AppSync_RetirementNarrowingTest.res +59 -46
  163. package/tests/AppSync_SdlDecorateTest.res +22 -20
  164. package/tests/AppSync_SourceApiAssociation_RetryingTest.res +40 -22
  165. package/tests/Auth_ActiveRolePoolAttachmentTest.res +114 -107
  166. package/tests/Auth_ActiveRoleStoreTest.res +12 -9
  167. package/tests/Auth_ActiveRoleStore_SchemaTest.res +2 -1
  168. package/tests/Auth_ActiveRoleTrigger_OpsTest.res +20 -16
  169. package/tests/Auth_CognitoTest.res +31 -46
  170. package/tests/AutomationSliceEntryPoint_OpsTest.res +69 -67
  171. package/tests/BakeConvergenceTest.res +22 -25
  172. package/tests/Capability_MessagingTest.res +28 -12
  173. package/tests/CommandTopicQueuePolicyTest.res +1 -2
  174. package/tests/CounterEntryPoint_OpsTest.res +3 -7
  175. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +306 -210
  176. package/tests/EventCollectorConnectLambdaPreviewTest.res +14 -7
  177. package/tests/EventCollectorEntryPoint_OpsTest.res +12 -4
  178. package/tests/EventCollectorPluginDefinitionAssetTest.res +18 -11
  179. package/tests/EventCollectorReadyGateTest.res +5 -4
  180. package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +1 -2
  181. package/tests/EventMapperEntryPoint_OpsTest.res +1 -3
  182. package/tests/ExtensionPointEntryPoint_OpsTest.res +1 -4
  183. package/tests/HandlerFactoryDispatchTest.res +97 -70
  184. package/tests/MCP_LambdaTest.res +16 -27
  185. package/tests/PgChangeFeedRelay_IntegrationTest.res +123 -93
  186. package/tests/PgChangeFeedRelay_RuntimeTest.res +13 -10
  187. package/tests/PgMigrationEntryPoint_IntegrationTest.res +0 -1
  188. package/tests/PgPipeline_IntegrationTest.res +80 -66
  189. package/tests/PgQueryDbUnion_IntegrationTest.res +1 -2
  190. package/tests/PgQueryResolverEntryPoint_OpsTest.res +2 -6
  191. package/tests/PgQueryResolver_LambdaTest.res +41 -24
  192. package/tests/Platform_AdminScan_OpsTest.res +4 -1
  193. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +137 -129
  194. package/tests/ProjectionEntryPoint_DisplayNameTest.res +7 -3
  195. package/tests/ProjectionEntryPoint_UnionStampTest.res +3 -2
  196. package/tests/ProvisionIdentityTest.res +2 -6
  197. package/tests/QueryDbOwnerIndexTableTest.res +6 -4
  198. package/tests/QueryDbResolvers_AppSyncTest.res +23 -16
  199. package/tests/QueryInterceptor_LambdaTest.res +11 -13
  200. package/tests/ReadModelEntryPoint_OpsTest.res +43 -41
  201. package/tests/RuntimeExtensionEntryPoint_OpsTest.res +4 -3
  202. package/tests/SideEffectEntryPoint_OpsTest.res +23 -18
  203. package/tests/StateChangeDescriptorParityTest.res +28 -26
  204. package/tests/StateTopicKeySchemaTest.res +18 -15
  205. package/tests/StateViewSliceEntryPoint_OpsTest.res +55 -44
  206. package/tests/TaskBucketEntryPoint_OpsTest.res +13 -13
  207. package/tests/Upload_ClaimTest.res +3 -3
  208. package/tests/Util_AwsErrorTest.res +4 -7
  209. package/tests/Util_BundleDependencyClosureTest.res +41 -3
  210. package/tests/Util_BundleDependencyClosureTest.res.mjs +11 -0
  211. package/tests/Util_BundleEsmLoaderTest.res +3 -3
  212. package/tests/Util_BundleResolveTest.res +4 -6
  213. package/tests/Util_BundleRuntimeExtensionTest.res +17 -18
  214. package/tests/Util_DcbMetricsTest.res +6 -6
  215. package/tests/Util_DynamoDbStreamTest.res +4 -5
  216. package/tests/Util_DynamoDbTest.res +3 -2
  217. package/tests/Util_LambdaEnvBudgetTest.res +22 -18
  218. package/tests/Util_LambdaLoggingTest.res +9 -8
  219. package/tests/Util_LogGroup_AdoptingTest.res +10 -5
  220. package/tests/Util_LogRetentionTest.res +6 -2
  221. package/tests/Util_OwnerScopeEnvTest.res +10 -8
  222. package/tests/Util_SQS_RuntimeTest.res +5 -7
  223. package/tests/Util_ShellConfigTest.res +7 -17
  224. package/tests/Util_StaticBundleTest.res +3 -8
  225. package/tests/Util_StoreLayoutTest.res +17 -12
  226. package/tests/integration/AggregateEntryPoint_IntegrationTest.res +1 -4
  227. package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res +15 -15
  228. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +296 -232
  229. package/tests/integration/DcbIntegrationHarness.res +8 -8
  230. package/tests/integration/EpCompositeSlice.res +16 -8
  231. package/tests/integration/QueryDbUnion_DynamoDb_IntegrationTest.res +8 -10
  232. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +10 -10
  233. package/tests/integration/SvsTestSlice_Projection.res +1 -3
  234. package/tests/util/Util_AlarmSpecTest.res +62 -49
@@ -64,104 +64,134 @@ describe("DCB DynamoDb integration — fence-scope = read-scope (Issue 1 regress
64
64
 
65
65
  // Place an order: read (orderId ∪ productId), then append OrderPlaced.
66
66
  let placeOrder = async (table, ~orderId, ~customerId) => {
67
- let query: Reventless.DcbTag.query = [
68
- {tags: [tag("orderId", orderId)]},
69
- {tags: [productTag]},
70
- ]
67
+ let query: Reventless.DcbTag.query = [{tags: [tag("orderId", orderId)]}, {tags: [productTag]}]
71
68
  let after = await readAfter(table, query)
72
69
  await Runtime.appendConditional(
73
70
  table,
74
71
  [event("OrderPlaced", [tag("orderId", orderId), tag("customerId", customerId), productTag])],
75
- {query, after: ?after},
72
+ {query, ?after},
76
73
  ~partitionTag=simple("orderId"),
77
74
  )
78
75
  }
79
76
 
80
- testAsync("a second order of the same product (different order + customer) succeeds", async () => {
81
- let table = await H.freshTable()
82
- let _ = await seed(table, event("CatalogProductSynced", [productTag]), ~partitionTag=simple("productId"))
77
+ testAsync(
78
+ "a second order of the same product (different order + customer) succeeds",
79
+ async () => {
80
+ let table = await H.freshTable()
81
+ let _ = await seed(
82
+ table,
83
+ event("CatalogProductSynced", [productTag]),
84
+ ~partitionTag=simple("productId"),
85
+ )
83
86
 
84
- let r1 = await placeOrder(table, ~orderId="O1", ~customerId="C1")
85
- let r2 = await placeOrder(table, ~orderId="O2", ~customerId="C2")
87
+ let r1 = await placeOrder(table, ~orderId="O1", ~customerId="C1")
88
+ let r2 = await placeOrder(table, ~orderId="O2", ~customerId="C2")
86
89
 
87
- expect(isOk(r1))->toBe(true)
88
- expect(isOk(r2))->toBe(true)
89
- })
90
+ expect(isOk(r1))->toBe(true)
91
+ expect(isOk(r2))->toBe(true)
92
+ },
93
+ )
90
94
 
91
- testAsync("two concurrent orders of the same product both succeed (no productId contention)", async () => {
92
- let table = await H.freshTable()
93
- let _ = await seed(table, event("CatalogProductSynced", [productTag]), ~partitionTag=simple("productId"))
95
+ testAsync(
96
+ "two concurrent orders of the same product both succeed (no productId contention)",
97
+ async () => {
98
+ let table = await H.freshTable()
99
+ let _ = await seed(
100
+ table,
101
+ event("CatalogProductSynced", [productTag]),
102
+ ~partitionTag=simple("productId"),
103
+ )
94
104
 
95
- let results = await Promise.all([
96
- placeOrder(table, ~orderId="OA", ~customerId="CA"),
97
- placeOrder(table, ~orderId="OB", ~customerId="CB"),
98
- ])
105
+ let results = await Promise.all([
106
+ placeOrder(table, ~orderId="OA", ~customerId="CA"),
107
+ placeOrder(table, ~orderId="OB", ~customerId="CB"),
108
+ ])
99
109
 
100
- expect(results->Array.every(isOk))->toBe(true)
101
- })
110
+ expect(results->Array.every(isOk))->toBe(true)
111
+ },
112
+ )
102
113
 
103
- testAsync("a concurrent product re-sync after the order's read still conflicts (OCC preserved)", async () => {
104
- let table = await H.freshTable()
105
- let _ = await seed(table, event("CatalogProductSynced", [productTag]), ~partitionTag=simple("productId"))
106
-
107
- // Order reads its decision model (after = the sync's position). Each clause
108
- // names the event type it reads — the per-type fence model checks the
109
- // consumed type's `pos#<type>`, so a tag-only clause would produce no fence
110
- // check at all.
111
- let query: Reventless.DcbTag.query = [
112
- {tags: [tag("orderId", "O1")], eventTypes: ["OrderPlaced"]},
113
- {tags: [productTag], eventTypes: ["CatalogProductSynced"]},
114
- ]
115
- let after = (await readAfter(table, query))->Option.getOr("")
116
- expect(after == "")->toBe(false)
117
-
118
- // …a concurrent re-sync advances fence#productId:P5's pos#CatalogProductSynced
119
- // past `after`. We set the fence directly (rather than racing a second
120
- // appendUnconditional) so the advance is deterministically > after —
121
- // same-millisecond writers can otherwise tie on UUID order (analysis Issue 7).
122
- // Appending "z" stays lexically greater than any real position.
123
- let _ = await H.setFence(table, productTag, ~eventTypes=["CatalogProductSynced"], ~position=after ++ "z")
124
-
125
- // …so the now-stale order append must conflict on the productId fence.
126
- let r = await Runtime.appendConditional(
127
- table,
128
- [event("OrderPlaced", [tag("orderId", "O1"), tag("customerId", "C1"), productTag])],
129
- {query, after: ?Some(after)},
130
- ~partitionTag=simple("orderId"),
131
- )
132
- expect(isConflict(r))->toBe(true)
133
- })
114
+ testAsync(
115
+ "a concurrent product re-sync after the order's read still conflicts (OCC preserved)",
116
+ async () => {
117
+ let table = await H.freshTable()
118
+ let _ = await seed(
119
+ table,
120
+ event("CatalogProductSynced", [productTag]),
121
+ ~partitionTag=simple("productId"),
122
+ )
134
123
 
135
- testAsync("placing an order does not poison the customer slice (secondary customerId not fenced)", async () => {
136
- let table = await H.freshTable()
137
- let customerTag = tag("customerId", "C1")
124
+ // Order reads its decision model (after = the sync's position). Each clause
125
+ // names the event type it reads — the per-type fence model checks the
126
+ // consumed type's `pos#<type>`, so a tag-only clause would produce no fence
127
+ // check at all.
128
+ let query: Reventless.DcbTag.query = [
129
+ {tags: [tag("orderId", "O1")], eventTypes: ["OrderPlaced"]},
130
+ {tags: [productTag], eventTypes: ["CatalogProductSynced"]},
131
+ ]
132
+ let after = (await readAfter(table, query))->Option.getOr("")
133
+ expect(after == "")->toBe(false)
134
+
135
+ // …a concurrent re-sync advances fence#productId:P5's pos#CatalogProductSynced
136
+ // past `after`. We set the fence directly (rather than racing a second
137
+ // appendUnconditional) so the advance is deterministically > after —
138
+ // same-millisecond writers can otherwise tie on UUID order (analysis Issue 7).
139
+ // Appending "z" stays lexically greater than any real position.
140
+ let _ = await H.setFence(
141
+ table,
142
+ productTag,
143
+ ~eventTypes=["CatalogProductSynced"],
144
+ ~position=after ++ "z",
145
+ )
138
146
 
139
- // Customer exists (partitioned by customerId) → fence#customerId:C1 seeded.
140
- let _ = await seed(table, event("CustomerRegistered", [customerTag]), ~partitionTag=simple("customerId"))
147
+ // …so the now-stale order append must conflict on the productId fence.
148
+ let r = await Runtime.appendConditional(
149
+ table,
150
+ [event("OrderPlaced", [tag("orderId", "O1"), tag("customerId", "C1"), productTag])],
151
+ {query, after: ?Some(after)},
152
+ ~partitionTag=simple("orderId"),
153
+ )
154
+ expect(isConflict(r))->toBe(true)
155
+ },
156
+ )
157
+
158
+ testAsync(
159
+ "placing an order does not poison the customer slice (secondary customerId not fenced)",
160
+ async () => {
161
+ let table = await H.freshTable()
162
+ let customerTag = tag("customerId", "C1")
163
+
164
+ // Customer exists (partitioned by customerId) → fence#customerId:C1 seeded.
165
+ let _ = await seed(
166
+ table,
167
+ event("CustomerRegistered", [customerTag]),
168
+ ~partitionTag=simple("customerId"),
169
+ )
141
170
 
142
- // Order for C1 carries customerId as a SECONDARY tag (partitioned by orderId).
143
- let orderQuery: Reventless.DcbTag.query = [{tags: [tag("orderId", "O1")]}]
144
- let orderAfter = await readAfter(table, orderQuery)
145
- let _ = await Runtime.appendConditional(
146
- table,
147
- [event("OrderPlaced", [tag("orderId", "O1"), customerTag, productTag])],
148
- {query: orderQuery, after: ?orderAfter},
149
- ~partitionTag=simple("orderId"),
150
- )
171
+ // Order for C1 carries customerId as a SECONDARY tag (partitioned by orderId).
172
+ let orderQuery: Reventless.DcbTag.query = [{tags: [tag("orderId", "O1")]}]
173
+ let orderAfter = await readAfter(table, orderQuery)
174
+ let _ = await Runtime.appendConditional(
175
+ table,
176
+ [event("OrderPlaced", [tag("orderId", "O1"), customerTag, productTag])],
177
+ {query: orderQuery, after: ?orderAfter},
178
+ ~partitionTag=simple("orderId"),
179
+ )
151
180
 
152
- // The customer slice reads its own partition (sees only CustomerRegistered)
153
- // and changes the email. Pre-fix the order would have bumped
154
- // fence#customerId:C1, falsely conflicting this append.
155
- let customerQuery: Reventless.DcbTag.query = [{tags: [customerTag]}]
156
- let customerAfter = await readAfter(table, customerQuery)
157
- let r = await Runtime.appendConditional(
158
- table,
159
- [event("EmailChanged", [customerTag])],
160
- {query: customerQuery, after: ?customerAfter},
161
- ~partitionTag=simple("customerId"),
162
- )
163
- expect(isOk(r))->toBe(true)
164
- })
181
+ // The customer slice reads its own partition (sees only CustomerRegistered)
182
+ // and changes the email. Pre-fix the order would have bumped
183
+ // fence#customerId:C1, falsely conflicting this append.
184
+ let customerQuery: Reventless.DcbTag.query = [{tags: [customerTag]}]
185
+ let customerAfter = await readAfter(table, customerQuery)
186
+ let r = await Runtime.appendConditional(
187
+ table,
188
+ [event("EmailChanged", [customerTag])],
189
+ {query: customerQuery, after: ?customerAfter},
190
+ ~partitionTag=simple("customerId"),
191
+ )
192
+ expect(isOk(r))->toBe(true)
193
+ },
194
+ )
165
195
  })
166
196
 
167
197
  describe("DCB DynamoDb integration — optimistic concurrency primitives", () => {
@@ -184,7 +214,11 @@ describe("DCB DynamoDb integration — optimistic concurrency primitives", () =>
184
214
  testAsync("two concurrent commits at the same after never both win (no duplicate)", async () => {
185
215
  let table = await H.freshTable()
186
216
  let counterTag = tag("counterId", "K1")
187
- let _ = await seed(table, event("CounterStarted", [counterTag]), ~partitionTag=simple("counterId"))
217
+ let _ = await seed(
218
+ table,
219
+ event("CounterStarted", [counterTag]),
220
+ ~partitionTag=simple("counterId"),
221
+ )
188
222
 
189
223
  let query: Reventless.DcbTag.query = [{tags: [counterTag]}]
190
224
  let after = await readAfter(table, query)
@@ -193,7 +227,7 @@ describe("DCB DynamoDb integration — optimistic concurrency primitives", () =>
193
227
  Runtime.appendConditional(
194
228
  table,
195
229
  [event("CounterIncremented", [counterTag])],
196
- {query, after: ?after},
230
+ {query, ?after},
197
231
  ~partitionTag=simple("counterId"),
198
232
  )
199
233
 
@@ -209,62 +243,72 @@ describe("DCB DynamoDb integration — optimistic concurrency primitives", () =>
209
243
  expect(conflicts >= 1)->toBe(true)
210
244
  })
211
245
 
212
- testAsync("two concurrent first-writers to the same entity — exactly one creates it (Issue 2)", async () => {
213
- let table = await H.freshTable()
214
- // Both writers read an empty decision model (after=None) and try to create
215
- // the same order. The per-(eventType, partition) create guard must let only
216
- // one through.
217
- let create = () =>
218
- Runtime.appendConditional(
246
+ testAsync(
247
+ "two concurrent first-writers to the same entity — exactly one creates it (Issue 2)",
248
+ async () => {
249
+ let table = await H.freshTable()
250
+ // Both writers read an empty decision model (after=None) and try to create
251
+ // the same order. The per-(eventType, partition) create guard must let only
252
+ // one through.
253
+ let create = () =>
254
+ Runtime.appendConditional(
255
+ table,
256
+ [event("OrderCreated", [tag("orderId", "O1")])],
257
+ {query: [{tags: [tag("orderId", "O1")]}], after: ?None},
258
+ ~partitionTag=simple("orderId"),
259
+ )
260
+
261
+ let results = await Promise.all([create(), create()])
262
+ let oks = results->Array.filter(isOk)->Array.length
263
+ // Safety: the two first-writers must NOT both create the entity. At most one
264
+ // commits; every non-winner surfaces a Conflict. (DynamoDB may cancel both
265
+ // with a mutual TransactionConflict — the slice callback's retry loop then
266
+ // makes exactly one win. Pre-fix this asserted-2: both committed = duplicate.)
267
+ expect(oks <= 1)->toBe(true)
268
+ expect(oks + results->Array.filter(isConflict)->Array.length)->toBe(2)
269
+
270
+ // No duplicate landed — persisted OrderCreated count matches the winners.
271
+ let readResult = await Runtime.read(table)(~query=[{tags: [tag("orderId", "O1")]}])
272
+ expect(readResult.events->Array.length)->toBe(oks)
273
+ },
274
+ )
275
+
276
+ testAsync(
277
+ "the create guard does not false-conflict a subset-event-type writer (Issue 4)",
278
+ async () => {
279
+ let table = await H.freshTable()
280
+ // A productId partition already holds a ProductAdded (its own create guard +
281
+ // fence). A different slice reads only NameChanged on that partition, sees
282
+ // nothing (after=None), and appends the first NameChanged. Its guard is
283
+ // create#NameChanged#productId:P — distinct from ProductAdded's — so it must
284
+ // NOT conflict with the pre-existing partition state.
285
+ let productTag = tag("productId", "P")
286
+ let r1 = await Runtime.appendConditional(
219
287
  table,
220
- [event("OrderCreated", [tag("orderId", "O1")])],
221
- {query: [{tags: [tag("orderId", "O1")]}], after: ?None},
222
- ~partitionTag=simple("orderId"),
288
+ [event("ProductAdded", [productTag])],
289
+ {query: [{tags: [productTag]}], after: ?None},
290
+ ~partitionTag=simple("productId"),
223
291
  )
292
+ expect(isOk(r1))->toBe(true)
224
293
 
225
- let results = await Promise.all([create(), create()])
226
- let oks = results->Array.filter(isOk)->Array.length
227
- // Safety: the two first-writers must NOT both create the entity. At most one
228
- // commits; every non-winner surfaces a Conflict. (DynamoDB may cancel both
229
- // with a mutual TransactionConflict — the slice callback's retry loop then
230
- // makes exactly one win. Pre-fix this asserted-2: both committed = duplicate.)
231
- expect(oks <= 1)->toBe(true)
232
- expect(oks + results->Array.filter(isConflict)->Array.length)->toBe(2)
233
-
234
- // No duplicate landed — persisted OrderCreated count matches the winners.
235
- let readResult = await Runtime.read(table)(~query=[{tags: [tag("orderId", "O1")]}])
236
- expect(readResult.events->Array.length)->toBe(oks)
237
- })
238
-
239
- testAsync("the create guard does not false-conflict a subset-event-type writer (Issue 4)", async () => {
240
- let table = await H.freshTable()
241
- // A productId partition already holds a ProductAdded (its own create guard +
242
- // fence). A different slice reads only NameChanged on that partition, sees
243
- // nothing (after=None), and appends the first NameChanged. Its guard is
244
- // create#NameChanged#productId:P — distinct from ProductAdded's — so it must
245
- // NOT conflict with the pre-existing partition state.
246
- let productTag = tag("productId", "P")
247
- let r1 = await Runtime.appendConditional(
248
- table,
249
- [event("ProductAdded", [productTag])],
250
- {query: [{tags: [productTag]}], after: ?None},
251
- ~partitionTag=simple("productId"),
252
- )
253
- expect(isOk(r1))->toBe(true)
254
-
255
- let r2 = await Runtime.appendConditional(
256
- table,
257
- [event("NameChanged", [productTag])],
258
- {query: [{tags: [productTag]}], after: ?None},
259
- ~partitionTag=simple("productId"),
260
- )
261
- expect(isOk(r2))->toBe(true)
262
- })
294
+ let r2 = await Runtime.appendConditional(
295
+ table,
296
+ [event("NameChanged", [productTag])],
297
+ {query: [{tags: [productTag]}], after: ?None},
298
+ ~partitionTag=simple("productId"),
299
+ )
300
+ expect(isOk(r2))->toBe(true)
301
+ },
302
+ )
263
303
 
264
304
  testAsync("a chain of compatible commits all succeed", async () => {
265
305
  let table = await H.freshTable()
266
306
  let counterTag = tag("counterId", "K2")
267
- let _ = await seed(table, event("CounterStarted", [counterTag]), ~partitionTag=simple("counterId"))
307
+ let _ = await seed(
308
+ table,
309
+ event("CounterStarted", [counterTag]),
310
+ ~partitionTag=simple("counterId"),
311
+ )
268
312
  let query: Reventless.DcbTag.query = [{tags: [counterTag]}]
269
313
 
270
314
  let step = async () => {
@@ -272,7 +316,7 @@ describe("DCB DynamoDb integration — optimistic concurrency primitives", () =>
272
316
  await Runtime.appendConditional(
273
317
  table,
274
318
  [event("CounterIncremented", [counterTag])],
275
- {query, after: ?after},
319
+ {query, ?after},
276
320
  ~partitionTag=simple("counterId"),
277
321
  )
278
322
  }
@@ -283,48 +327,51 @@ describe("DCB DynamoDb integration — optimistic concurrency primitives", () =>
283
327
  expect([r1, r2, r3]->Array.every(isOk))->toBe(true)
284
328
  })
285
329
 
286
- testAsync("a failed fence condition aborts the whole transaction (multi-tag atomicity)", async () => {
287
- let table = await H.freshTable()
288
- // Composite slice: one multi-tag clause → both tags get check+bump. The
289
- // clause names the consumed type so the per-type fence check has a `pos#<type>`
290
- // to assert against.
291
- let tags = [tag("productId", "p1"), tag("orderId", "o1")]
292
- let query: Reventless.DcbTag.query = [{tags: tags, eventTypes: ["ProductDemandRecorded"]}]
293
-
294
- // Seed one event so the next append rides after=Some.
295
- let seedRes = await Runtime.appendConditional(
296
- table,
297
- [event("ProductDemandRecorded", tags)],
298
- {query, after: ?None},
299
- ~partitionTag=simple("productId"),
300
- )
301
- expect(isOk(seedRes))->toBe(true)
302
-
303
- let after = (await readAfter(table, query))->Option.getOr("")
304
- expect(after == "")->toBe(false)
330
+ testAsync(
331
+ "a failed fence condition aborts the whole transaction (multi-tag atomicity)",
332
+ async () => {
333
+ let table = await H.freshTable()
334
+ // Composite slice: one multi-tag clause → both tags get check+bump. The
335
+ // clause names the consumed type so the per-type fence check has a `pos#<type>`
336
+ // to assert against.
337
+ let tags = [tag("productId", "p1"), tag("orderId", "o1")]
338
+ let query: Reventless.DcbTag.query = [{tags, eventTypes: ["ProductDemandRecorded"]}]
339
+
340
+ // Seed one event so the next append rides after=Some.
341
+ let seedRes = await Runtime.appendConditional(
342
+ table,
343
+ [event("ProductDemandRecorded", tags)],
344
+ {query, after: ?None},
345
+ ~partitionTag=simple("productId"),
346
+ )
347
+ expect(isOk(seedRes))->toBe(true)
305
348
 
306
- // A concurrent writer advances ONLY the productId fence's pos#ProductDemandRecorded
307
- // past `after`. Appending "z" keeps the position lexically greater than any real one.
308
- let _ = await H.setFence(
309
- table,
310
- tag("productId", "p1"),
311
- ~eventTypes=["ProductDemandRecorded"],
312
- ~position=after ++ "z",
313
- )
349
+ let after = (await readAfter(table, query))->Option.getOr("")
350
+ expect(after == "")->toBe(false)
314
351
 
315
- let r = await Runtime.appendConditional(
316
- table,
317
- [event("ProductDemandRecorded", tags)],
318
- {query, after: ?Some(after)},
319
- ~partitionTag=simple("productId"),
320
- )
321
- expect(isConflict(r))->toBe(true)
352
+ // A concurrent writer advances ONLY the productId fence's pos#ProductDemandRecorded
353
+ // past `after`. Appending "z" keeps the position lexically greater than any real one.
354
+ let _ = await H.setFence(
355
+ table,
356
+ tag("productId", "p1"),
357
+ ~eventTypes=["ProductDemandRecorded"],
358
+ ~position=after ++ "z",
359
+ )
322
360
 
323
- // Atomic rollback: the second event was NOT written — the composite read
324
- // still returns exactly the one seeded event.
325
- let readResult = await Runtime.read(table)(~query)
326
- expect(readResult.events->Array.length)->toBe(1)
327
- })
361
+ let r = await Runtime.appendConditional(
362
+ table,
363
+ [event("ProductDemandRecorded", tags)],
364
+ {query, after: ?Some(after)},
365
+ ~partitionTag=simple("productId"),
366
+ )
367
+ expect(isConflict(r))->toBe(true)
368
+
369
+ // Atomic rollback: the second event was NOT written — the composite read
370
+ // still returns exactly the one seeded event.
371
+ let readResult = await Runtime.read(table)(~query)
372
+ expect(readResult.events->Array.length)->toBe(1)
373
+ },
374
+ )
328
375
  })
329
376
 
330
377
  // A `@compositePartitionTag` slice (platform-inspector's SyncResource shape) fences
@@ -367,27 +414,30 @@ describe("DCB DynamoDb integration — composite partition hot-fence fix", () =>
367
414
  expect(isOk(rB))->toBe(true)
368
415
  })
369
416
 
370
- testAsync("two first-writers of the SAME composite key still serialize (OCC preserved)", async () => {
371
- let table = await H.freshTable()
372
- let tags = members("resA")
373
- let query: Reventless.DcbTag.query = [{tags: tags}]
374
- let r1 = await Runtime.appendConditional(
375
- table,
376
- [event("ResourceAdded", tags)],
377
- {query, after: ?None},
378
- ~partitionTag=pt,
379
- )
380
- let r2 = await Runtime.appendConditional(
381
- table,
382
- [event("ResourceAdded", tags)],
383
- {query, after: ?None},
384
- ~partitionTag=pt,
385
- )
386
- expect(isOk(r1))->toBe(true)
387
- // The folded create guard (attribute_not_exists on the composite fence) rejects
388
- // the second first-write of the same entity.
389
- expect(isConflict(r2))->toBe(true)
390
- })
417
+ testAsync(
418
+ "two first-writers of the SAME composite key still serialize (OCC preserved)",
419
+ async () => {
420
+ let table = await H.freshTable()
421
+ let tags = members("resA")
422
+ let query: Reventless.DcbTag.query = [{tags: tags}]
423
+ let r1 = await Runtime.appendConditional(
424
+ table,
425
+ [event("ResourceAdded", tags)],
426
+ {query, after: ?None},
427
+ ~partitionTag=pt,
428
+ )
429
+ let r2 = await Runtime.appendConditional(
430
+ table,
431
+ [event("ResourceAdded", tags)],
432
+ {query, after: ?None},
433
+ ~partitionTag=pt,
434
+ )
435
+ expect(isOk(r1))->toBe(true)
436
+ // The folded create guard (attribute_not_exists on the composite fence) rejects
437
+ // the second first-write of the same entity.
438
+ expect(isConflict(r2))->toBe(true)
439
+ },
440
+ )
391
441
  })
392
442
 
393
443
  // The fence is PER event type (`pos#<eventType>` attributes), not a single scalar
@@ -408,55 +458,69 @@ describe("DCB DynamoDb integration — per-type fence granularity", () => {
408
458
  await Runtime.appendConditional(
409
459
  table,
410
460
  [event(eventType, [productTag])],
411
- {query, after: ?after},
461
+ {query, ?after},
412
462
  ~partitionTag=simple("productId"),
413
463
  )
414
464
  }
415
465
 
416
- testAsync("interleaved distinct-type changes on one product all succeed (never wedges)", async () => {
417
- let table = await H.freshTable()
418
- let _ = await seed(table, event("ProductAdded", [productTag]), ~partitionTag=simple("productId"))
419
-
420
- // Two rounds of interleaved price/name/description changes. Pre-fix, the first
421
- // PriceChanged bumped the single partition fence, so the following NameChanged
422
- // read a stale head and conflicted PERMANENTLY — the entity wedged after one edit.
423
- let results = [
424
- await change(table, ~eventType="PriceChanged"),
425
- await change(table, ~eventType="NameChanged"),
426
- await change(table, ~eventType="DescriptionChanged"),
427
- await change(table, ~eventType="PriceChanged"),
428
- await change(table, ~eventType="NameChanged"),
429
- ]
430
- expect(results->Array.every(isOk))->toBe(true)
431
- })
432
-
433
- testAsync("two concurrent SAME-type changes still serialize (per-type OCC preserved)", async () => {
434
- let table = await H.freshTable()
435
- let _ = await seed(table, event("ProductAdded", [productTag]), ~partitionTag=simple("productId"))
436
- // Seed one NameChanged so both racers read the same after=Some head.
437
- let _ = await change(table, ~eventType="NameChanged")
466
+ testAsync(
467
+ "interleaved distinct-type changes on one product all succeed (never wedges)",
468
+ async () => {
469
+ let table = await H.freshTable()
470
+ let _ = await seed(
471
+ table,
472
+ event("ProductAdded", [productTag]),
473
+ ~partitionTag=simple("productId"),
474
+ )
438
475
 
439
- let query: Reventless.DcbTag.query = [{tags: [productTag], eventTypes: ["NameChanged"]}]
440
- let after = await readAfter(table, query)
441
- let rename = () =>
442
- Runtime.appendConditional(
476
+ // Two rounds of interleaved price/name/description changes. Pre-fix, the first
477
+ // PriceChanged bumped the single partition fence, so the following NameChanged
478
+ // read a stale head and conflicted PERMANENTLY — the entity wedged after one edit.
479
+ let results = [
480
+ await change(table, ~eventType="PriceChanged"),
481
+ await change(table, ~eventType="NameChanged"),
482
+ await change(table, ~eventType="DescriptionChanged"),
483
+ await change(table, ~eventType="PriceChanged"),
484
+ await change(table, ~eventType="NameChanged"),
485
+ ]
486
+ expect(results->Array.every(isOk))->toBe(true)
487
+ },
488
+ )
489
+
490
+ testAsync(
491
+ "two concurrent SAME-type changes still serialize (per-type OCC preserved)",
492
+ async () => {
493
+ let table = await H.freshTable()
494
+ let _ = await seed(
443
495
  table,
444
- [event("NameChanged", [productTag])],
445
- {query, after: ?after},
496
+ event("ProductAdded", [productTag]),
446
497
  ~partitionTag=simple("productId"),
447
498
  )
499
+ // Seed one NameChanged so both racers read the same after=Some head.
500
+ let _ = await change(table, ~eventType="NameChanged")
448
501
 
449
- // Per-type granularity must NOT weaken same-type OCC: two concurrent NameChanged
450
- // at the same `pos#NameChanged` head → at most one commits, every non-winner
451
- // conflicts. (DynamoDB may cancel both with a mutual TransactionConflict; the
452
- // slice callback's retry loop makes exactly one win in prod.)
453
- let results = await Promise.all([rename(), rename()])
454
- let oks = results->Array.filter(isOk)->Array.length
455
- let conflicts = results->Array.filter(isConflict)->Array.length
456
- expect(oks <= 1)->toBe(true)
457
- expect(oks + conflicts)->toBe(2)
458
- expect(conflicts >= 1)->toBe(true)
459
- })
502
+ let query: Reventless.DcbTag.query = [{tags: [productTag], eventTypes: ["NameChanged"]}]
503
+ let after = await readAfter(table, query)
504
+ let rename = () =>
505
+ Runtime.appendConditional(
506
+ table,
507
+ [event("NameChanged", [productTag])],
508
+ {query, ?after},
509
+ ~partitionTag=simple("productId"),
510
+ )
511
+
512
+ // Per-type granularity must NOT weaken same-type OCC: two concurrent NameChanged
513
+ // at the same `pos#NameChanged` head → at most one commits, every non-winner
514
+ // conflicts. (DynamoDB may cancel both with a mutual TransactionConflict; the
515
+ // slice callback's retry loop makes exactly one win in prod.)
516
+ let results = await Promise.all([rename(), rename()])
517
+ let oks = results->Array.filter(isOk)->Array.length
518
+ let conflicts = results->Array.filter(isConflict)->Array.length
519
+ expect(oks <= 1)->toBe(true)
520
+ expect(oks + conflicts)->toBe(2)
521
+ expect(conflicts >= 1)->toBe(true)
522
+ },
523
+ )
460
524
  })
461
525
 
462
526
  // An empty tag value is a legitimate model state (an absent member of a composite