refactor: remove localLamport completely from sync system

- Remove localLamport and lastSyncedLamport from all interfaces and models
- Simplify sync logic to use vector clocks exclusively for change tracking
- Remove Lamport-based conflict detection and sync status logic
- Update UI components to remove Lamport display elements
- Clean up backwards compatibility utilities
- Fix getVectorClock import error in sync.service.ts
- Remove debug alert from sync-safety-backup.service.ts

This simplifies the sync system by eliminating the dual tracking
mechanism that was causing complications and false conflicts.
Vector clocks provide better causality tracking for distributed sync.
This commit is contained in:
Johannes Millan 2025-07-05 18:20:24 +02:00
parent 48bff6b46d
commit 658c29a8d7
12 changed files with 91 additions and 668 deletions

View file

@ -106,31 +106,23 @@ if (comparison === VectorClockComparison.CONCURRENT) {
2. **SyncService**: Merges vector clocks during download, includes in upload
3. **getSyncStatusFromMetaFiles**: Uses vector clocks for conflict detection
## Migration from Lamport Timestamps
## Vector Clock Implementation
The system maintains backwards compatibility with existing Lamport timestamps:
The system uses vector clocks exclusively for conflict detection:
### Automatic Migration
### How It Works
```typescript
// If no vector clock exists, create from Lamport timestamp
if (!meta.vectorClock && meta.localLamport > 0) {
meta.vectorClock = { [clientId]: meta.localLamport };
}
```
- Each client maintains its own counter in the vector clock
- Counters increment on local changes only
- Vector clocks are compared to detect concurrent changes
- No false conflicts from timestamp-based comparisons
### Dual Support
### Current Fields
- New installations use vector clocks immediately
- Existing installations migrate transparently
- Both systems coexist during transition period
### Field Mapping
| Old Field | New Field | Purpose |
| ------------------- | ----------------------- | ------------------- |
| `localLamport` | `vectorClock[clientId]` | Track local changes |
| `lastSyncedLamport` | `lastSyncedVectorClock` | Track sync state |
| Field | Purpose |
| ----------------------- | -------------------------------- |
| `vectorClock` | Track changes across all clients |
| `lastSyncedVectorClock` | Track last synced state |
## API Reference

View file

@ -66,18 +66,11 @@
<thead>
<tr>
<th></th>
<th>{{ T.F.SYNC.D_CONFLICT.LAMPORT_CLOCK | translate }}</th>
<th>{{ T.F.SYNC.D_CONFLICT.LAST_WRITE | translate }}</th>
</tr>
</thead>
<tr>
<td>{{ T.F.SYNC.D_CONFLICT.REMOTE | translate }}</td>
<td
[matTooltip]="remote.localLamport"
[attr.data-label]="T.F.SYNC.D_CONFLICT.LAMPORT_CLOCK | translate"
>
{{ shortenLamport(remote.localLamport) }}
</td>
<td
[matTooltip]="remote.lastUpdateAction"
[attr.data-label]="T.F.SYNC.D_CONFLICT.LAST_WRITE | translate"
@ -87,12 +80,6 @@
</tr>
<tr>
<td>{{ T.F.SYNC.D_CONFLICT.LOCAL | translate }}</td>
<td
[matTooltip]="local.localLamport"
[attr.data-label]="T.F.SYNC.D_CONFLICT.LAMPORT_CLOCK | translate"
>
{{ shortenLamport(local.localLamport) }}
</td>
<td
[matTooltip]="local.lastUpdateAction"
[attr.data-label]="T.F.SYNC.D_CONFLICT.LAST_WRITE | translate"

View file

@ -88,11 +88,6 @@ export class DialogSyncConflictComponent {
}
}
shortenLamport(lamport?: number | null): string {
if (!lamport) return '-';
return lamport.toString().slice(-5);
}
shortenAction(actionStr?: string | null): string {
if (!actionStr) return '?';
return actionStr.trim().split(/\s+/)[0];
@ -149,11 +144,8 @@ export class DialogSyncConflictComponent {
return changeCount;
}
// Fallback to Lamport clock
const lastSyncedLamport = this.local.lastSyncedLamport || 0;
const currentLamport =
side === 'remote' ? this.remote.localLamport : this.local.localLamport;
return Math.max(0, currentLamport - lastSyncedLamport);
// No vector clock available
return 0;
}
private shouldConfirmOverwrite(resolution: DialogConflictResolutionResult): boolean {

View file

@ -33,7 +33,6 @@ export class SyncSafetyBackupService {
constructor() {
// Subscribe to the onBeforeUpdateLocal event
this._pfapiService.pf.ev.on('onBeforeUpdateLocal', async (eventData) => {
alert(2);
try {
pfLog(1, 'SyncSafetyBackupService: Received onBeforeUpdateLocal event', {
modelsToUpdate: eventData.modelsToUpdate,

View file

@ -139,8 +139,6 @@ export class SyncWrapperService {
localVectorClock: r.conflictData?.local.vectorClock,
remoteVectorClock: r.conflictData?.remote.vectorClock,
localLastSyncedVectorClock: r.conflictData?.local.lastSyncedVectorClock,
localLamport: r.conflictData?.local.localLamport,
remoteLamport: r.conflictData?.remote.localLamport,
conflictReason: r.conflictData?.reason,
additional: r.conflictData?.additional,
});

View file

@ -12,7 +12,6 @@ import { validateLocalMeta } from '../util/validate-local-meta';
import { PFEventEmitter } from '../util/events';
import { devError } from '../../../util/dev-error';
import { incrementVectorClock, limitVectorClockSize } from '../util/vector-clock';
import { getVectorClock, withVectorClock } from '../util/backwards-compat';
export const DEFAULT_META_MODEL: LocalMeta = {
crossModelVersion: 1,
@ -20,8 +19,6 @@ export const DEFAULT_META_MODEL: LocalMeta = {
lastUpdate: 0,
metaRev: null,
lastSyncedUpdate: null,
localLamport: 0,
lastSyncedLamport: null,
vectorClock: {},
lastSyncedVectorClock: null,
};
@ -92,32 +89,18 @@ export class MetaModelCtrl {
const lastUpdateAction =
actionStr.length > 100 ? actionStr.substring(0, 97) + '...' : actionStr;
// Update vector clock - migrate from Lamport if needed
let currentVectorClock = getVectorClock(metaModel, clientId);
if (!currentVectorClock && metaModel.localLamport > 0) {
// First time creating vector clock - migrate from Lamport timestamp
currentVectorClock = { [clientId]: metaModel.localLamport };
pfLog(
2,
`${MetaModelCtrl.L}.${this.updateRevForModel.name}() migrating Lamport to vector clock`,
{
clientId,
localLamport: metaModel.localLamport,
newVectorClock: currentVectorClock,
},
);
}
let newVectorClock = incrementVectorClock(currentVectorClock || {}, clientId);
// Update vector clock
const currentVectorClock = metaModel.vectorClock || {};
let newVectorClock = incrementVectorClock(currentVectorClock, clientId);
// Apply size limiting to prevent unbounded growth
newVectorClock = limitVectorClockSize(newVectorClock, clientId);
const baseUpdatedMeta = {
const updatedMeta = {
...metaModel,
lastUpdate: timestamp,
lastUpdateAction,
localLamport: this._incrementLamport(metaModel.localLamport || 0),
vectorClock: newVectorClock,
...(modelCfg.isMainFileModel
? {}
@ -131,9 +114,6 @@ export class MetaModelCtrl {
crossModelVersion: this.crossModelVersion,
};
// Create final meta with vector clock using pure function
const updatedMeta = withVectorClock(baseUpdatedMeta, newVectorClock, clientId);
await this.save(updatedMeta, isIgnoreDBLock);
}
@ -152,14 +132,6 @@ export class MetaModelCtrl {
lastUpdate: metaModel.lastUpdate,
isIgnoreDBLock,
});
if (typeof metaModel.lastUpdate !== 'number') {
return Promise.reject(
new InvalidMetaError(
`${MetaModelCtrl.L}.${this.save.name}()`,
'lastUpdate not found',
),
);
}
// NOTE: in order to not mess up separate model updates started at the same time, we need to update synchronously as well
this._metaModelInMemory = validateLocalMeta(metaModel);
@ -186,7 +158,6 @@ export class MetaModelCtrl {
pfLog(
2,
`${MetaModelCtrl.L}.${this.save.name}() DB save completed successfully`,
metaModel.localLamport,
metaModel,
);
})
@ -247,14 +218,6 @@ export class MetaModelCtrl {
vectorClockKeys: data.vectorClock ? Object.keys(data.vectorClock) : [],
});
// Ensure Lamport fields are initialized for old data
if (data.localLamport === undefined) {
data.localLamport = 0;
}
if (data.lastSyncedLamport === undefined) {
data.lastSyncedLamport = null;
}
// Ensure vector clock fields are initialized for old data
if (data.vectorClock === undefined) {
data.vectorClock = {};
@ -335,11 +298,6 @@ export class MetaModelCtrl {
// Increment this client's vector clock component
metaData.vectorClock[clientId] = (metaData.vectorClock[clientId] || 0) + 1;
// Also increment the Lamport timestamp for backwards compatibility
if (typeof metaData.localLamport === 'number') {
metaData.localLamport = this._incrementLamport(metaData.localLamport);
}
// Save the updated metadata
await this.save({
...metaData,
@ -389,19 +347,4 @@ export class MetaModelCtrl {
pfLog(2, `${MetaModelCtrl.L}.${this._generateClientId.name}()`);
return getEnvironmentId() + '_' + Date.now();
}
/**
* Safely increments Lamport timestamp with overflow protection
*
* @param current Current Lamport value
* @returns Incremented value
*/
private _incrementLamport(current: number): number {
// Reset if approaching max safe integer
if (current >= Number.MAX_SAFE_INTEGER - 1000) {
pfLog(1, `${MetaModelCtrl.L} Lamport counter overflow protection triggered`);
return 1;
}
return current + 1;
}
}

View file

@ -91,12 +91,6 @@ export interface MetaFileBase {
lastUpdateAction?: string;
revMap: RevMap;
crossModelVersion: number;
// Change tracking fields - support both old and new names for backwards compatibility
localLamport: number;
lastSyncedLamport: number | null;
// New field names (will be migrated to gradually)
localChangeCounter?: number;
lastSyncedChangeCounter?: number | null;
lastSyncedAction?: string;
// Vector clock fields for improved conflict detection
vectorClock?: VectorClock;
@ -112,13 +106,9 @@ export interface RemoteMeta extends MetaFileBase {
isFullData?: boolean;
}
export interface UploadMeta
extends Omit<
RemoteMeta,
'lastSyncedLamport' | 'lastSyncedChangeCounter' | 'lastSyncedVectorClock'
> {
lastSyncedLamport: null;
lastSyncedChangeCounter?: null;
export interface UploadMeta extends Omit<RemoteMeta, 'lastSyncedVectorClock'> {
// Vector clock should not be synced, only used locally
lastSyncedVectorClock?: null;
}
export interface LocalMeta extends MetaFileBase {

View file

@ -21,12 +21,11 @@ Located in `sync-providers/`:
### Sync Algorithm
The sync system uses a hybrid approach combining:
The sync system uses vector clocks for accurate conflict detection:
1. **Physical Timestamps** (`lastUpdate`) - For ordering events
2. **Change Counters** (`localLamport`) - For detecting local modifications
3. **Vector Clocks** (`vectorClock`) - For accurate causality tracking
4. **Sync State** (`lastSyncedUpdate`, `lastSyncedLamport`) - To track last successful sync
2. **Vector Clocks** (`vectorClock`) - For accurate causality tracking and conflict detection
3. **Sync State** (`lastSyncedUpdate`, `lastSyncedVectorClock`) - To track last successful sync
## How Sync Works
@ -37,7 +36,6 @@ When a user modifies data:
```typescript
// In meta-model-ctrl.ts
lastUpdate = Date.now();
localLamport = localLamport + 1;
vectorClock[clientId] = vectorClock[clientId] + 1;
```
@ -75,9 +73,8 @@ if (comparison === VectorClockComparison.CONCURRENT) {
interface LocalMeta {
lastUpdate: number; // Physical timestamp
lastSyncedUpdate: number; // Last synced timestamp
localLamport: number; // Change counter
lastSyncedLamport: number; // Last synced counter
vectorClock?: VectorClock; // Causality tracking
lastSyncedVectorClock?: VectorClock; // Last synced vector clock
revMap: RevMap; // Model revision map
crossModelVersion: number; // Schema version
}
@ -85,9 +82,9 @@ interface LocalMeta {
### Important Considerations
1. **NOT Pure Lamport Clocks**: We don't increment on receive to prevent sync loops
1. **Vector Clocks**: Each client maintains its own counter for accurate causality tracking
2. **Backwards Compatibility**: Supports migration from older versions
3. **Conflict Minimization**: Vector clocks reduce false conflicts
3. **Conflict Minimization**: Vector clocks eliminate false conflicts
4. **Atomic Operations**: Meta file serves as transaction coordinator
## Common Sync Scenarios

View file

@ -35,26 +35,16 @@ import {
limitVectorClockSize,
sanitizeVectorClock,
} from '../util/vector-clock';
import {
getVectorClock,
withVectorClock,
withLastSyncedVectorClock,
} from '../util/backwards-compat';
import { getVectorClock } from '../util/backwards-compat';
/**
* Sync Service for Super Productivity
*
* Change Detection System:
* This is NOT a pure Lamport timestamp implementation!
* We use a hybrid approach that combines:
* - Physical timestamps (lastUpdate) for ordering
* - Change counters (localLamport) for detecting local modifications
* - Sync tracking (lastSyncedLamport) to detect when sync is needed
*
* Key difference from Lamport clocks:
* - We DON'T increment on receive (prevents sync loops)
* - We DO increment on local changes
* - We track the last synced state separately
* Uses vector clocks for detecting concurrent changes and conflicts between devices.
* Each client maintains its own counter in the vector clock which is incremented
* on local changes. This allows proper conflict detection when changes happen
* on multiple devices simultaneously.
*/
export class SyncService<const MD extends ModelCfgs> {
public readonly IS_DO_CROSS_MODEL_MIGRATIONS: boolean;
@ -198,36 +188,24 @@ export class SyncService<const MD extends ModelCfgs> {
return { status };
case SyncStatus.InSync:
// Ensure lastSyncedUpdate is set even when already in sync
if (
localMeta.lastSyncedUpdate !== localMeta.lastUpdate ||
localMeta.lastSyncedLamport !== localMeta.localLamport
) {
pfLog(2, 'InSync but lastSyncedUpdate/Lamport needs update', {
if (localMeta.lastSyncedUpdate !== localMeta.lastUpdate) {
pfLog(2, 'InSync but lastSyncedUpdate needs update', {
lastSyncedUpdate: localMeta.lastSyncedUpdate,
lastUpdate: localMeta.lastUpdate,
lastSyncedLamport: localMeta.lastSyncedLamport,
localLamport: localMeta.localLamport,
});
// Get client ID for vector clock operations
const clientId = await this._metaModelCtrl.loadClientId();
const localVector = getVectorClock(localMeta, clientId);
// Get the local vector clock
const localVector = localMeta.vectorClock;
let updatedMeta = {
const updatedMeta = {
...localMeta,
lastSyncedUpdate: localMeta.lastUpdate,
lastSyncedLamport: localMeta.localLamport || 0,
metaRev: remoteMetaRev,
// Ensure vector clock fields are always present
vectorClock: localMeta.vectorClock || {},
lastSyncedVectorClock: localMeta.lastSyncedVectorClock || null,
lastSyncedVectorClock: localVector || null,
};
// Update vector clock if available
if (localVector) {
updatedMeta = withLastSyncedVectorClock(updatedMeta, localVector, clientId);
}
await this._metaFileSyncService.saveLocal(updatedMeta);
}
return { status };
@ -278,47 +256,29 @@ export class SyncService<const MD extends ModelCfgs> {
if (isForceUpload) {
// Get client ID for vector clock operations
const clientId = await this._metaModelCtrl.loadClientId();
let localVector = getVectorClock(local, clientId) || {};
let localVector = local.vectorClock || {};
// For conflict resolution, fetch remote metadata once and handle both Lamport and vector clocks
// For conflict resolution, fetch remote metadata once and handle vector clocks
let remoteMeta: RemoteMeta | null = null;
let remoteLamport = 0;
try {
const result = await this._metaFileSyncService.download();
remoteMeta = result.remoteMeta;
remoteLamport = remoteMeta.localLamport || 0;
} catch (e) {
pfLog(1, 'Warning: Cannot fetch remote metadata during force upload', e);
// If download fails, use local values as baseline
remoteLamport = local.localLamport || 0;
}
// Set our Lamport to be higher than both local and remote
const currentLamport = Math.max(local.localLamport || 0, remoteLamport);
// Reset if approaching max safe integer (same logic as MetaModelCtrl)
const nextLamport =
currentLamport >= Number.MAX_SAFE_INTEGER - 1000 ? 1 : currentLamport + 1;
pfLog(
2,
`${SyncService.L}.${this.uploadAll.name}(): Incrementing change counter for conflict resolution`,
{ localLamport: local.localLamport, remoteLamport, currentLamport, nextLamport },
);
// Merge vector clocks if remote metadata was successfully fetched
if (remoteMeta) {
let remoteVector = getVectorClock(remoteMeta, clientId);
if (remoteVector) {
// Sanitize remote vector clock before merging
remoteVector = sanitizeVectorClock(remoteVector);
localVector = mergeVectorClocks(localVector, remoteVector);
pfLog(2, 'Merged remote vector clock for force upload', {
localOriginal: getVectorClock(local, clientId),
remote: remoteVector,
merged: localVector,
});
}
if (remoteMeta && remoteMeta.vectorClock) {
let remoteVector = remoteMeta.vectorClock;
// Sanitize remote vector clock before merging
remoteVector = sanitizeVectorClock(remoteVector);
localVector = mergeVectorClocks(localVector, remoteVector);
pfLog(2, 'Merged remote vector clock for force upload', {
localOriginal: local.vectorClock,
remote: remoteVector,
merged: localVector,
});
} else {
pfLog(1, 'Proceeding with force upload without remote vector clock merge');
}
@ -328,17 +288,12 @@ export class SyncService<const MD extends ModelCfgs> {
// Apply size limiting to prevent unbounded growth
newVector = limitVectorClockSize(newVector, clientId);
let updatedMeta = {
const updatedMeta = {
...local,
lastUpdate: Date.now(),
localLamport: nextLamport,
// Important: Don't update lastSyncedLamport yet
// It will be updated after successful upload
vectorClock: newVector,
};
// Update vector clock
updatedMeta = withVectorClock(updatedMeta, newVector, clientId);
await this._metaModelCtrl.save(
updatedMeta,
// NOTE we always ignore db lock while syncing
@ -348,9 +303,8 @@ export class SyncService<const MD extends ModelCfgs> {
}
try {
// Get client ID for vector clock operations
const clientId = await this._metaModelCtrl.loadClientId();
const localVector = getVectorClock(local, clientId);
// Get the local vector clock
const localVector = local.vectorClock;
return await this.uploadToRemote(
{
@ -359,8 +313,6 @@ export class SyncService<const MD extends ModelCfgs> {
revMap: {},
// Will be assigned later
mainModelData: {},
localLamport: local.localLamport || 0,
lastSyncedLamport: null,
vectorClock: localVector,
},
{
@ -368,7 +320,6 @@ export class SyncService<const MD extends ModelCfgs> {
revMap: this._fakeFullRevMap(),
// Ensure lastSyncedUpdate matches lastUpdate to prevent false conflicts
lastSyncedUpdate: local.lastUpdate,
lastSyncedLamport: local.localLamport || 0,
},
null,
);
@ -400,8 +351,6 @@ export class SyncService<const MD extends ModelCfgs> {
lastSyncedUpdate: null,
metaRev: null,
revMap: {},
localLamport: 0,
lastSyncedLamport: null,
// Include vector clock fields to prevent comparison issues
vectorClock: {},
lastSyncedVectorClock: null,
@ -471,29 +420,20 @@ export class SyncService<const MD extends ModelCfgs> {
mergedVector = limitVectorClockSize(mergedVector, clientId);
}
let updatedMeta = {
const updatedMeta = {
// shared
lastUpdate: remote.lastUpdate,
crossModelVersion: remote.crossModelVersion,
revMap: remote.revMap,
// Don't increment localLamport during download - only take the max
// localLamport tracks LOCAL changes only
localLamport: Math.max(local.localLamport || 0, remote.localLamport || 0),
// local meta
lastSyncedUpdate: remote.lastUpdate,
lastSyncedLamport: Math.max(local.localLamport || 0, remote.localLamport || 0),
metaRev: remoteRev,
// Always include vector clock fields to prevent them from being lost
vectorClock: mergedVector || local.vectorClock || remote.vectorClock || {},
lastSyncedVectorClock: null,
lastSyncedVectorClock:
mergedVector || local.vectorClock || remote.vectorClock || {},
};
// Update vector clocks if we have them
if (mergedVector) {
updatedMeta = withVectorClock(updatedMeta, mergedVector, clientId);
updatedMeta = withLastSyncedVectorClock(updatedMeta, mergedVector, clientId);
}
await this._metaFileSyncService.saveLocal(updatedMeta);
return;
}
@ -586,14 +526,10 @@ export class SyncService<const MD extends ModelCfgs> {
mergedVector = limitVectorClockSize(mergedVector, clientId);
}
let updatedMeta = {
const updatedMeta = {
metaRev: remoteRev,
lastSyncedUpdate: remote.lastUpdate,
lastUpdate: remote.lastUpdate,
// Don't increment localLamport during download - only take the max
// localLamport tracks LOCAL changes only
localLamport: Math.max(local.localLamport || 0, remote.localLamport || 0),
lastSyncedLamport: Math.max(local.localLamport || 0, remote.localLamport || 0),
lastSyncedAction: `Downloaded ${isDownloadAll ? 'all data' : `${toUpdate.length} models`} at ${new Date().toISOString()}`,
revMap: validateRevMap({
...local.revMap,
@ -602,15 +538,10 @@ export class SyncService<const MD extends ModelCfgs> {
crossModelVersion: remote.crossModelVersion,
// Always include vector clock fields to prevent them from being lost
vectorClock: mergedVector || local.vectorClock || remote.vectorClock || {},
lastSyncedVectorClock: null,
lastSyncedVectorClock:
mergedVector || local.vectorClock || remote.vectorClock || {},
};
// Update vector clocks if we have them
if (mergedVector) {
updatedMeta = withVectorClock(updatedMeta, mergedVector, clientId);
updatedMeta = withLastSyncedVectorClock(updatedMeta, mergedVector, clientId);
}
await this._metaFileSyncService.saveLocal(updatedMeta);
}
@ -646,19 +577,13 @@ export class SyncService<const MD extends ModelCfgs> {
? await this._pfapiMain.getAllSyncModelData()
: await this._modelSyncService.getMainFileModelDataForUpload();
// Get client ID for vector clock operations
const clientId = await this._metaModelCtrl.loadClientId();
const localVector = getVectorClock(local, clientId);
const uploadMeta: UploadMeta = {
revMap: local.revMap,
lastUpdate: local.lastUpdate,
lastUpdateAction: local.lastUpdateAction,
crossModelVersion: local.crossModelVersion,
mainModelData,
localLamport: local.localLamport || 0,
lastSyncedLamport: null,
vectorClock: localVector,
vectorClock: local.vectorClock,
...(syncProvider.isLimitedToSingleFileSync ? { isFullData: true } : {}),
};
@ -679,19 +604,14 @@ export class SyncService<const MD extends ModelCfgs> {
},
);
let updatedMeta = {
const updatedMeta = {
...local,
lastSyncedUpdate: local.lastUpdate,
lastSyncedLamport: local.localLamport || 0,
lastSyncedAction: `Uploaded single file at ${new Date().toISOString()}`,
metaRev: metaRevAfterUpdate,
lastSyncedVectorClock: local.vectorClock || null,
};
// Update vector clock if available
if (localVector) {
updatedMeta = withLastSyncedVectorClock(updatedMeta, localVector, clientId);
}
await this._metaFileSyncService.saveLocal(updatedMeta);
pfLog(
@ -759,10 +679,6 @@ export class SyncService<const MD extends ModelCfgs> {
// Validate and upload the final revMap
const validatedRevMap = validateRevMap(realRevMap);
// Get client ID for vector clock operations
const clientId = await this._metaModelCtrl.loadClientId();
const localVector = getVectorClock(local, clientId);
const uploadMeta: UploadMeta = {
revMap: validatedRevMap,
lastUpdate: local.lastUpdate,
@ -770,36 +686,27 @@ export class SyncService<const MD extends ModelCfgs> {
crossModelVersion: local.crossModelVersion,
mainModelData:
await this._modelSyncService.getMainFileModelDataForUpload(completeData),
localLamport: local.localLamport || 0,
lastSyncedLamport: null,
vectorClock: localVector,
vectorClock: local.vectorClock,
};
const metaRevAfterUpload = await this._metaFileSyncService.upload(uploadMeta);
// Update local after successful upload
let updatedMeta = {
const updatedMeta = {
// leave as is basically
lastUpdate: local.lastUpdate,
crossModelVersion: local.crossModelVersion,
localLamport: local.localLamport || 0,
// Always include vector clock fields to prevent them from being lost
vectorClock: local.vectorClock || {},
lastSyncedVectorClock: local.lastSyncedVectorClock || null,
lastSyncedVectorClock: local.vectorClock || null,
// actual updates
lastSyncedUpdate: local.lastUpdate,
lastSyncedLamport: local.localLamport || 0,
lastSyncedAction: `Uploaded ${toUpdate.length} models at ${new Date().toISOString()}`,
revMap: validatedRevMap,
metaRev: metaRevAfterUpload,
};
// Update vector clock if available
if (localVector) {
updatedMeta = withLastSyncedVectorClock(updatedMeta, localVector, clientId);
}
await this._metaFileSyncService.saveLocal(updatedMeta);
}

View file

@ -1,271 +1,11 @@
import { LocalMeta, RemoteMeta, VectorClock } from '../pfapi.model';
import { lamportToVectorClock, isVectorClockEmpty } from './vector-clock';
import { pfLog } from './log';
import { LocalMeta, RemoteMeta } from '../pfapi.model';
import { isVectorClockEmpty } from './vector-clock';
/**
* Utility functions for backwards compatibility with old field names.
* This allows gradual migration from localLamport/lastSyncedLamport to
* localChangeCounter/lastSyncedChangeCounter and to vector clocks.
* Utility functions for backwards compatibility.
* Now focused on vector clock utilities only.
*/
/**
* Get the local change counter value, checking both old and new field names
*/
export const getLocalChangeCounter = (meta: LocalMeta | RemoteMeta): number => {
// Prefer new field name if available
if (meta.localChangeCounter !== undefined) {
return meta.localChangeCounter;
}
// Fall back to old field name
return meta.localLamport || 0;
};
/**
* Get the last synced change counter value, checking both old and new field names
*/
export const getLastSyncedChangeCounter = (
meta: LocalMeta | RemoteMeta,
): number | null => {
// Prefer new field name if available
if (meta.lastSyncedChangeCounter !== undefined) {
return meta.lastSyncedChangeCounter;
}
// Fall back to old field name, ensuring we return null if undefined
return meta.lastSyncedLamport ?? null;
};
/**
* Create a new metadata object with the local change counter value set,
* updating both old and new field names for backwards compatibility
*/
export const withLocalChangeCounter = <T extends LocalMeta | RemoteMeta>(
meta: T,
value: number,
): T => {
return {
...meta,
localLamport: value,
localChangeCounter: value,
};
};
/**
* Create a new metadata object with the last synced change counter value set,
* updating both old and new field names for backwards compatibility
*/
export const withLastSyncedChangeCounter = <T extends LocalMeta | RemoteMeta>(
meta: T,
value: number | null,
): T => {
return {
...meta,
lastSyncedLamport: value,
lastSyncedChangeCounter: value,
};
};
/**
* @deprecated Use withLocalChangeCounter instead - this mutates the object
*/
export const setLocalChangeCounter = (
meta: LocalMeta | RemoteMeta,
value: number,
): void => {
const updated = withLocalChangeCounter(meta, value);
Object.assign(meta, updated);
};
/**
* @deprecated Use withLastSyncedChangeCounter instead - this mutates the object
*/
export const setLastSyncedChangeCounter = (
meta: LocalMeta | RemoteMeta,
value: number | null,
): void => {
const updated = withLastSyncedChangeCounter(meta, value);
Object.assign(meta, updated);
};
/**
* Create a metadata object with both old and new field names populated
*/
export const createBackwardsCompatibleMeta = <T extends LocalMeta | RemoteMeta>(
meta: T,
): T => {
const result = { ...meta };
// Ensure both field names are populated
if (result.localChangeCounter !== undefined && result.localLamport === undefined) {
result.localLamport = result.localChangeCounter;
} else if (
result.localLamport !== undefined &&
result.localChangeCounter === undefined
) {
result.localChangeCounter = result.localLamport;
} else if (
result.localChangeCounter !== undefined &&
result.localLamport !== undefined &&
result.localChangeCounter !== result.localLamport
) {
// Warn about field mismatch but use the newer field
pfLog(1, 'WARN: Mismatch between localChangeCounter and localLamport fields', {
localChangeCounter: result.localChangeCounter,
localLamport: result.localLamport,
using: 'localChangeCounter',
});
result.localLamport = result.localChangeCounter;
}
if (
result.lastSyncedChangeCounter !== undefined &&
result.lastSyncedLamport === undefined
) {
result.lastSyncedLamport = result.lastSyncedChangeCounter;
} else if (
result.lastSyncedLamport !== undefined &&
result.lastSyncedChangeCounter === undefined
) {
result.lastSyncedChangeCounter = result.lastSyncedLamport;
} else if (
result.lastSyncedChangeCounter !== undefined &&
result.lastSyncedLamport !== undefined &&
result.lastSyncedChangeCounter !== result.lastSyncedLamport
) {
// Warn about field mismatch but use the newer field
pfLog(
1,
'WARN: Mismatch between lastSyncedChangeCounter and lastSyncedLamport fields',
{
lastSyncedChangeCounter: result.lastSyncedChangeCounter,
lastSyncedLamport: result.lastSyncedLamport,
using: 'lastSyncedChangeCounter',
},
);
result.lastSyncedLamport = result.lastSyncedChangeCounter;
}
return result;
};
/**
* Get the vector clock, creating it from Lamport timestamp if needed
* @param meta The metadata object
* @param clientId The client ID to use for migration
* @returns The vector clock
*/
export const getVectorClock = (
meta: LocalMeta | RemoteMeta,
clientId: string,
): VectorClock | undefined => {
// Return existing vector clock if available
if (meta.vectorClock && !isVectorClockEmpty(meta.vectorClock)) {
return meta.vectorClock;
}
// Migrate from Lamport timestamp if available
const changeCounter = getLocalChangeCounter(meta);
if (changeCounter > 0) {
return lamportToVectorClock(changeCounter, clientId);
}
return undefined;
};
/**
* Get the last synced vector clock, creating it from Lamport timestamp if needed
* @param meta The metadata object
* @param clientId The client ID to use for migration
* @returns The last synced vector clock
*/
export const getLastSyncedVectorClock = (
meta: LocalMeta | RemoteMeta,
clientId: string,
): VectorClock | null => {
// Return existing vector clock if available
if (meta.lastSyncedVectorClock && !isVectorClockEmpty(meta.lastSyncedVectorClock)) {
return meta.lastSyncedVectorClock;
}
// Migrate from Lamport timestamp if available
const lastSyncedCounter = getLastSyncedChangeCounter(meta);
if (lastSyncedCounter != null && lastSyncedCounter > 0) {
return lamportToVectorClock(lastSyncedCounter, clientId);
}
return null;
};
/**
* Create a new metadata object with the vector clock set and Lamport timestamps updated
* @param meta The metadata object
* @param vectorClock The vector clock to set
* @param clientId The client ID for this instance
* @returns A new metadata object with updated vector clock
*/
export const withVectorClock = <T extends LocalMeta | RemoteMeta>(
meta: T,
vectorClock: VectorClock,
clientId: string,
): T => {
// Update Lamport timestamps for backwards compatibility
// Use this client's component value
const clientValue = vectorClock[clientId] || 0;
return {
...meta,
vectorClock,
localLamport: clientValue,
localChangeCounter: clientValue,
};
};
/**
* Create a new metadata object with the last synced vector clock set and Lamport timestamps updated
* @param meta The metadata object
* @param vectorClock The vector clock to set (can be null)
* @param clientId The client ID for this instance
* @returns A new metadata object with updated last synced vector clock
*/
export const withLastSyncedVectorClock = <T extends LocalMeta | RemoteMeta>(
meta: T,
vectorClock: VectorClock | null,
clientId: string,
): T => {
// Update Lamport timestamps for backwards compatibility
const lastSyncedValue = vectorClock ? vectorClock[clientId] || 0 : null;
return {
...meta,
lastSyncedVectorClock: vectorClock,
lastSyncedLamport: lastSyncedValue,
lastSyncedChangeCounter: lastSyncedValue,
};
};
/**
* @deprecated Use withVectorClock instead - this mutates the object
*/
export const setVectorClock = (
meta: LocalMeta | RemoteMeta,
vectorClock: VectorClock,
clientId: string,
): void => {
const updated = withVectorClock(meta, vectorClock, clientId);
Object.assign(meta, updated);
};
/**
* @deprecated Use withLastSyncedVectorClock instead - this mutates the object
*/
export const setLastSyncedVectorClock = (
meta: LocalMeta | RemoteMeta,
vectorClock: VectorClock | null,
clientId: string,
): void => {
const updated = withLastSyncedVectorClock(meta, vectorClock, clientId);
Object.assign(meta, updated);
};
/**
* Check if both metadata objects have vector clocks
* @param local Local metadata
@ -277,3 +17,16 @@ export const hasVectorClocks = (local: LocalMeta, remote: RemoteMeta): boolean =
!isVectorClockEmpty(local.vectorClock) && !isVectorClockEmpty(remote.vectorClock)
);
};
/**
* Get the vector clock from metadata
* @param meta Metadata object
* @param clientId Client ID (unused, kept for compatibility)
* @returns Vector clock or null if not present
*/
export const getVectorClock = (
meta: LocalMeta | RemoteMeta,
clientId?: string,
): Record<string, number> | null => {
return meta.vectorClock || null;
};

View file

@ -7,11 +7,7 @@ import {
SyncInvalidTimeValuesError,
} from '../errors/errors';
import { pfLog } from './log';
import {
getLocalChangeCounter,
getLastSyncedChangeCounter,
hasVectorClocks,
} from './backwards-compat';
import { hasVectorClocks } from './backwards-compat';
import {
compareVectorClocks,
VectorClockComparison,
@ -123,67 +119,20 @@ export const getSyncStatusFromMetaFiles = (
}
}
// Enhanced fallback: Try to create hybrid comparison using available data
const localChangeCounter = getLocalChangeCounter(local);
const remoteChangeCounter = getLocalChangeCounter(remote);
const lastSyncedChangeCounter = getLastSyncedChangeCounter(local);
// Handle mixed vector clock states gracefully for migration
if (localHasVectorClock !== remoteHasVectorClock) {
pfLog(
2,
'Mixed vector clock state detected - using migration-friendly comparison',
'Mixed vector clock state detected - using timestamp comparison for migration',
{
localHasVectorClock,
remoteHasVectorClock,
localChangeCounter,
remoteChangeCounter,
lastSyncedChangeCounter,
localLastUpdate: local.lastUpdate,
remoteLastUpdate: remote.lastUpdate,
localLastSyncedUpdate: local.lastSyncedUpdate,
},
);
// If we have valid change counters (non-zero), use them for comparison during migration
const hasValidChangeCounters =
typeof localChangeCounter === 'number' &&
typeof remoteChangeCounter === 'number' &&
typeof lastSyncedChangeCounter === 'number' &&
(localChangeCounter > 0 ||
remoteChangeCounter > 0 ||
lastSyncedChangeCounter > 0);
if (hasValidChangeCounters) {
// Use Lamport comparison logic for mixed states
const hasLocalChanges = localChangeCounter > lastSyncedChangeCounter;
const hasRemoteChanges = remoteChangeCounter > lastSyncedChangeCounter;
if (!hasLocalChanges && !hasRemoteChanges) {
return { status: SyncStatus.InSync };
} else if (hasLocalChanges && !hasRemoteChanges) {
return { status: SyncStatus.UpdateRemote };
} else if (!hasLocalChanges && hasRemoteChanges) {
return { status: SyncStatus.UpdateLocal };
} else {
// Both have changes - compare values
if (localChangeCounter > remoteChangeCounter) {
return { status: SyncStatus.UpdateRemote };
} else if (remoteChangeCounter > localChangeCounter) {
return { status: SyncStatus.UpdateLocal };
} else {
// Equal change counters with changes - likely same changes
return { status: SyncStatus.InSync };
}
}
}
// If no valid change counters, fall back to timestamp comparison
pfLog(2, 'No valid change counters in mixed state - using timestamp comparison', {
localHasVectorClock,
remoteHasVectorClock,
localLastUpdate: local.lastUpdate,
remoteLastUpdate: remote.lastUpdate,
localLastSyncedUpdate: local.lastSyncedUpdate,
});
// If we have lastSyncedUpdate, check for changes
if (typeof local.lastSyncedUpdate === 'number') {
const hasLocalChanges = local.lastUpdate > local.lastSyncedUpdate;
@ -225,55 +174,7 @@ export const getSyncStatusFromMetaFiles = (
}
}
// Standard Lamport fallback when both sides lack vector clocks
if (
typeof localChangeCounter === 'number' &&
typeof remoteChangeCounter === 'number' &&
typeof lastSyncedChangeCounter === 'number'
) {
const lamportResult = _checkForUpdateLamport({
remoteLocalLamport: remoteChangeCounter,
localLamport: localChangeCounter,
lastSyncedLamport: lastSyncedChangeCounter,
});
pfLog(2, 'Using change counters for sync status', {
localChangeCounter,
remoteChangeCounter,
lastSyncedChangeCounter,
result: lamportResult,
hasLocalChanges: localChangeCounter > lastSyncedChangeCounter,
hasRemoteChanges: remoteChangeCounter > lastSyncedChangeCounter,
});
switch (lamportResult) {
case UpdateCheckResult.InSync:
return {
status: SyncStatus.InSync,
};
case UpdateCheckResult.LocalUpdateRequired:
return {
status: SyncStatus.UpdateLocal,
};
case UpdateCheckResult.RemoteUpdateRequired:
return {
status: SyncStatus.UpdateRemote,
};
case UpdateCheckResult.DataDiverged:
return {
status: SyncStatus.Conflict,
conflictData: {
reason: ConflictReason.BothNewerLastSync,
remote,
local,
},
};
}
}
// TODO remove later once it is likely that all running apps have lamport clocks
// Final fallback to timestamp-based checking
// Fallback to timestamp-based checking when vector clocks are not available
if (typeof local.lastSyncedUpdate === 'number') {
const r = _checkForUpdate({
@ -425,42 +326,6 @@ const _checkForUpdateVectorClock = (params: {
}
};
const _checkForUpdateLamport = (params: {
remoteLocalLamport: number;
localLamport: number;
lastSyncedLamport: number;
}): UpdateCheckResult => {
const { remoteLocalLamport, localLamport, lastSyncedLamport } = params;
pfLog(2, 'Lamport timestamp check', {
localLamport,
remoteLocalLamport,
lastSyncedLamport,
hasLocalChanges: localLamport > lastSyncedLamport,
hasRemoteChanges: remoteLocalLamport > lastSyncedLamport,
});
// Check if there have been changes since last sync
const hasLocalChanges = localLamport > lastSyncedLamport;
const hasRemoteChanges = remoteLocalLamport > lastSyncedLamport;
if (!hasLocalChanges && !hasRemoteChanges) {
return UpdateCheckResult.InSync;
} else if (hasLocalChanges && !hasRemoteChanges) {
return UpdateCheckResult.RemoteUpdateRequired;
} else if (!hasLocalChanges && hasRemoteChanges) {
return UpdateCheckResult.LocalUpdateRequired;
} else {
// Both have changes - check if they're the same
if (localLamport === remoteLocalLamport) {
// Both made the same changes - they're in sync
return UpdateCheckResult.InSync;
}
// Different changes - conflict
return UpdateCheckResult.DataDiverged;
}
};
const _checkForUpdate = (params: {
remote: number;
local: number;

View file

@ -4,7 +4,7 @@ import { environment } from '../../../../environments/environment';
2: normal
3: verbose
*/
const LOG_LEVEL = environment.production ? 2 : 0;
const LOG_LEVEL = environment.production ? 2 : 2;
/**
* Safe logging function that prevents crashes during console access