66
77import { calculateChecksum } from '../packer.js' ;
88import type { StorageBackend } from '../../storage/index.js' ;
9- import type { Manifest , LocalSyncState } from '../../types/index.js' ;
9+ import type { Manifest , LocalSyncState , SyncCategory } from '../../types/index.js' ;
1010import type { CategoryData , StorageFiles } from '../operations/types.js' ;
11- import { isBlobCategoryData } from '../operations/types.js' ;
11+ import { isBlobCategoryData , isItemCategoryData } from '../operations/types.js' ;
1212
1313/**
1414 * Build updated local state after a sync operation.
@@ -23,14 +23,17 @@ export function buildLocalState(
2323 const now = new Date ( ) . toISOString ( ) ;
2424 const checksums : LocalSyncState [ 'categoryChecksums' ] = { } ;
2525 const baseVersions : LocalSyncState [ 'baseVersions' ] = { } ;
26+ const itemChecksums : Partial < Record < SyncCategory , Record < string , string > > > = { } ;
2627
2728 for ( const item of data ) {
28- // Only blob categories have a single data string for checksum/base tracking
2929 if ( isBlobCategoryData ( item ) ) {
30+ // Blob categories: track single checksum and base version for three-way merge
3031 checksums [ item . category ] = calculateChecksum ( item . data ) ;
3132 baseVersions [ item . category ] = item . data ;
33+ } else if ( isItemCategoryData ( item ) ) {
34+ // Per-item categories: track individual item checksums for deletion detection
35+ itemChecksums [ item . category ] = item . checksums ;
3236 }
33- // Per-item categories don't need base versions (they use additive merge)
3437 }
3538
3639 return {
@@ -41,6 +44,7 @@ export function buildLocalState(
4144 vectorClock : manifest . vectorClock ,
4245 categoryChecksums : checksums ,
4346 baseVersions,
47+ itemChecksums,
4448 } ;
4549}
4650
@@ -60,6 +64,68 @@ export function isLockedByOther(
6064 return Date . now ( ) - lockTime < timeout ;
6165}
6266
67+ /** Build a lookup map from category array */
68+ function buildCategoryMap ( data : CategoryData [ ] ) : Map < SyncCategory , CategoryData > {
69+ const map = new Map < SyncCategory , CategoryData > ( ) ;
70+ for ( const item of data ) map . set ( item . category , item ) ;
71+ return map ;
72+ }
73+
74+ /** Merge item category data (local + pulled) */
75+ import type { ItemCategoryData } from '../operations/types.js' ;
76+
77+ function mergeItemData ( local : ItemCategoryData , pulled : ItemCategoryData ) : CategoryData {
78+ return {
79+ category : pulled . category ,
80+ type : 'items' ,
81+ items : { ...local . items , ...pulled . items } ,
82+ checksums : { ...local . checksums , ...pulled . checksums } ,
83+ } ;
84+ }
85+
86+ /** Process a single pulled category, merging with local if needed */
87+ function processPulledCategory (
88+ pulled : CategoryData ,
89+ localByCategory : Map < SyncCategory , CategoryData >
90+ ) : CategoryData {
91+ if ( isBlobCategoryData ( pulled ) ) return pulled ;
92+ if ( ! isItemCategoryData ( pulled ) ) return pulled ;
93+
94+ const local = localByCategory . get ( pulled . category ) ;
95+ if ( local && isItemCategoryData ( local ) ) {
96+ return mergeItemData ( local , pulled ) ;
97+ }
98+ return pulled ;
99+ }
100+
101+ /**
102+ * Merge local and pulled data for complete state tracking.
103+ * After a pull, we need to track ALL items (local + pulled) for proper deletion detection.
104+ */
105+ export function mergeDataForState (
106+ localData : CategoryData [ ] | undefined ,
107+ pulledData : CategoryData [ ]
108+ ) : CategoryData [ ] {
109+ if ( ! localData ) return pulledData ;
110+
111+ const localByCategory = buildCategoryMap ( localData ) ;
112+ const processedCategories = new Set < SyncCategory > ( ) ;
113+ const result : CategoryData [ ] = [ ] ;
114+
115+ // Process pulled data first (it takes precedence for blobs)
116+ for ( const pulled of pulledData ) {
117+ processedCategories . add ( pulled . category ) ;
118+ result . push ( processPulledCategory ( pulled , localByCategory ) ) ;
119+ }
120+
121+ // Add local-only categories (not in pulled)
122+ for ( const local of localData ) {
123+ if ( ! processedCategories . has ( local . category ) ) result . push ( local ) ;
124+ }
125+
126+ return result ;
127+ }
128+
63129/**
64130 * Build storage files map from backend listing.
65131 */
0 commit comments