@@ -112,7 +112,7 @@ export class BucketChecksumState {
112112
113113 /** Set of all buckets in this checkpoint. */
114114 const bucketDescriptionMap = new Map (
115- allBuckets . map ( ( b ) => [ b . bucket , this . parameterState . overrideBucketDescription ( b ) ] )
115+ allBuckets . map ( ( b ) => [ b . bucket , this . parameterState . translateResolvedBucket ( b ) ] )
116116 ) ;
117117
118118 if ( bucketDescriptionMap . size > this . context . maxBuckets ) {
@@ -345,7 +345,7 @@ export interface CheckpointUpdate {
345345 /**
346346 * All buckets forming part of the checkpoint.
347347 */
348- buckets : BucketDescription [ ] ;
348+ buckets : ResolvedBucket [ ] ;
349349
350350 /**
351351 * If present, a set of buckets that have been updated since the last checkpoint.
@@ -367,7 +367,7 @@ export class BucketParameterState {
367367 private readonly explicitStreamSubscriptions : Record < string , util . RequestedStreamSubscription > ;
368368 private readonly subscribedStreamNames : Set < string > ;
369369 private readonly logger : Logger ;
370- private cachedDynamicBuckets : BucketDescription [ ] | null = null ;
370+ private cachedDynamicBuckets : ResolvedBucket [ ] | null = null ;
371371 private cachedDynamicBucketSet : Set < string > | null = null ;
372372
373373 private readonly lookups : Set < string > ;
@@ -412,7 +412,10 @@ export class BucketParameterState {
412412 hasDefaultStreams : this . includeDefaultStreams ,
413413 streams : streamsByName
414414 } ) ;
415- this . staticBuckets = new Map < string , BucketDescription > ( this . querier . staticBuckets . map ( ( b ) => [ b . bucket , b ] ) ) ;
415+
416+ this . staticBuckets = new Map < string , BucketDescription > (
417+ mergeBuckets ( this . querier . staticBuckets ) . map ( ( b ) => [ b . bucket , b ] )
418+ ) ;
416419 this . lookups = new Set < string > ( this . querier . parameterQueryLookups . map ( ( l ) => JSONBig . stringify ( l . values ) ) ) ;
417420 this . subscribedStreamNames = new Set ( Object . keys ( streamsByName ) ) ;
418421 }
@@ -422,7 +425,8 @@ export class BucketParameterState {
422425 * {@link util.ClientBucketDescription}.
423426 */
424427 translateResolvedBucket ( description : ResolvedBucket ) : util . ClientBucketDescription {
425- // Assign
428+ // If the client is overriding the priority of any stream that yields this bucket, sync the bucket with that
429+ // priority.
426430 let priorityOverride : BucketPriority | null = null ;
427431 for ( const reason of description . inclusion_reasons ) {
428432 if ( reason != 'default' ) {
@@ -531,7 +535,7 @@ export class BucketParameterState {
531535 }
532536 }
533537
534- let dynamicBuckets : BucketDescription [ ] ;
538+ let dynamicBuckets : ResolvedBucket [ ] ;
535539 if ( hasParameterChange || this . cachedDynamicBuckets == null || this . cachedDynamicBucketSet == null ) {
536540 dynamicBuckets = await querier . queryDynamicBucketDescriptions ( {
537541 getParameterSets ( lookups ) {
@@ -612,3 +616,32 @@ function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number)
612616 const limited = buckets . slice ( 0 , limit ) ;
613617 return `${ JSON . stringify ( limited ) } ...` ;
614618}
619+
620+ /**
621+ * Resolves duplicate buckets in the given array, merging the inclusion reasons for duplicate.
622+ *
623+ * It's possible for duplicates to occur when a stream has multiple subscriptions, consider e.g.
624+ *
625+ * ```
626+ * sync_streams:
627+ * assets_by_category:
628+ * query: select * from assets where category in (request.parameters() -> 'categories')
629+ * ```
630+ *
631+ * Here, a client might subscribe once with `{"categories": [1]}` and once with `{"categories": [1, 2]}`. Since each
632+ * subscription is evaluated independently, this would lead to three buckets, with a duplicate `assets_by_category[1]`
633+ * bucket.
634+ */
635+ function mergeBuckets ( buckets : ResolvedBucket [ ] ) : ResolvedBucket [ ] {
636+ const byDefinition : Record < string , ResolvedBucket > = { } ;
637+
638+ for ( const bucket of buckets ) {
639+ if ( Object . hasOwn ( byDefinition , bucket . definition ) ) {
640+ byDefinition [ bucket . definition ] . inclusion_reasons . push ( ...bucket . inclusion_reasons ) ;
641+ } else {
642+ byDefinition [ bucket . definition ] = bucket ;
643+ }
644+ }
645+
646+ return Object . values ( byDefinition ) ;
647+ }
0 commit comments