@@ -253,17 +253,26 @@ export class SessionBuffer extends BaseBuffer {
253253 }
254254
255255 /**
256- * Squash multiple (sign=-1, sign=+1) pairs for the same session id into
257- * just the boundary rows: the oldest -1 (cancels the previous CH state)
258- * and the newest +1 (the final state) . Single-row sessions (no update
259- * pair yet) pass through unchanged.
256+ * Squash the (sign=-1, sign=+1) rows for each session id down to the
257+ * minimal set that produces the same collapsed state in ClickHouse, by
258+ * netting the signs per version . Single-row sessions pass through
259+ * unchanged.
260260 *
261- * Correctness depends on the invariant maintained by `getSession`: every
262- * update for an existing session emits exactly one -1 at version V and
263- * one +1 at V+1, where V is the existing version. The oldest -1 in the
264- * batch cancels CH's previous +1, and the newest +1 becomes the new
265- * canonical row. Intermediate (-1, +1) pairs cancel each other in CH
266- * either way — keeping them is wasted work.
261+ * `sessions` uses VersionedCollapsingMergeTree(sign, version): a (-1, V)
262+ * row only collapses against a (+1, V) row of the SAME version. So the
263+ * net effect of a batch on a session is computed per-version — sum the
264+ * signs at each version and emit only the rows needed to realise that
265+ * net. Versions that fully cancel (net 0) are dropped; the surviving
266+ * rows are normally the final (+1, maxVersion) plus, for a mid-life
267+ * session, the (-1, V) that cancels CH's resident row.
268+ *
269+ * This per-version netting is required because a session's creation row
270+ * (+1 at the base version) can land in the SAME batch as its updates.
271+ * The first update emits a (-1, baseVersion) whose only partner is that
272+ * in-batch creation (+1, baseVersion). They must cancel each other here;
273+ * naively keeping "oldest -1 + newest +1" would drop the creation +1
274+ * while keeping its canceller, leaving a permanently un-collapsible (-1)
275+ * row in CH (duplicate session rows + negative bounce counts).
267276 */
268277 private squashSessionsByVersion (
269278 sessions : IClickhouseSession [ ]
@@ -274,12 +283,11 @@ export class SessionBuffer extends BaseBuffer {
274283
275284 const grouped = new Map < string , IClickhouseSession [ ] > ( ) ;
276285 for ( const s of sessions ) {
277- const key = s . id ;
278- const arr = grouped . get ( key ) ;
286+ const arr = grouped . get ( s . id ) ;
279287 if ( arr ) {
280288 arr . push ( s ) ;
281289 } else {
282- grouped . set ( key , [ s ] ) ;
290+ grouped . set ( s . id , [ s ] ) ;
283291 }
284292 }
285293
@@ -291,27 +299,36 @@ export class SessionBuffer extends BaseBuffer {
291299 out . push ( entries [ 0 ] ! ) ;
292300 continue ;
293301 }
294- let oldestNeg : IClickhouseSession | null = null ;
295- let newestPos : IClickhouseSession | null = null ;
302+
303+ const netByVersion = new Map <
304+ number ,
305+ { net : number ; pos ?: IClickhouseSession ; neg ?: IClickhouseSession }
306+ > ( ) ;
296307 for ( const e of entries ) {
297- if ( e . sign === - 1 ) {
298- if ( ! oldestNeg || e . version < oldestNeg . version ) {
299- oldestNeg = e ;
300- }
301- } else if (
302- e . sign === 1 &&
303- ( ! newestPos || e . version > newestPos . version )
304- ) {
305- newestPos = e ;
308+ const slot = netByVersion . get ( e . version ) ?? { net : 0 } ;
309+ slot . net += e . sign ;
310+ if ( e . sign === 1 ) {
311+ slot . pos = e ;
312+ } else if ( e . sign === - 1 ) {
313+ slot . neg = e ;
306314 }
315+ netByVersion . set ( e . version , slot ) ;
307316 }
317+
308318 const emitted : IClickhouseSession [ ] = [ ] ;
309- if ( oldestNeg ) {
310- emitted . push ( oldestNeg ) ;
311- }
312- if ( newestPos ) {
313- emitted . push ( newestPos ) ;
319+ for ( const slot of netByVersion . values ( ) ) {
320+ if ( slot . net === 0 ) {
321+ continue ;
322+ }
323+ const row = slot . net > 0 ? slot . pos : slot . neg ;
324+ if ( ! row ) {
325+ continue ;
326+ }
327+ for ( let i = 0 ; i < Math . abs ( slot . net ) ; i ++ ) {
328+ emitted . push ( row ) ;
329+ }
314330 }
331+
315332 squashedCount += entries . length - emitted . length ;
316333 out . push ( ...emitted ) ;
317334 }
@@ -328,17 +345,6 @@ export class SessionBuffer extends BaseBuffer {
328345 ) ;
329346 }
330347
331- console . log (
332- squashedCount > 0
333- ? 'Session batch squashed'
334- : 'Session batch not squashed' ,
335- {
336- inputRows : sessions . length ,
337- outputRows : out . length ,
338- dropped : squashedCount ,
339- }
340- ) ;
341-
342348 return out ;
343349 }
344350
0 commit comments