feat: sync client versioning (#5645)

after this pr, server will only accept client that have some major version
the client version <0.12 will be rejected by the server, >= 0.12 can receive outdated messages and notify users
This commit is contained in:
DarkSky
2024-02-05 08:43:50 +00:00
parent 5ca0d65241
commit 25e8a2a22f
15 changed files with 144 additions and 143 deletions

View File

@@ -1,15 +1,23 @@
export enum SyncEngineStep {
// error
Rejected = -1,
// in progress
Stopped = 0,
Syncing = 1,
// finished
Synced = 2,
}
export enum SyncPeerStep {
// error
VersionRejected = -1,
// in progress
Stopped = 0,
Retrying = 1,
LoadingRootDoc = 2,
LoadingSubDoc = 3,
Loaded = 4.5,
Syncing = 5,
// finished
Synced = 6,
}

View File

@@ -13,6 +13,7 @@ export interface SyncEngineStatus {
step: SyncEngineStep;
local: SyncPeerStatus | null;
remotes: (SyncPeerStatus | null)[];
error: string | null;
retrying: boolean;
}
@@ -82,6 +83,7 @@ export class SyncEngine {
step: SyncEngineStep.Stopped,
local: null,
remotes: remotes.map(() => null),
error: null,
retrying: false,
};
}
@@ -130,6 +132,7 @@ export class SyncEngine {
step: SyncEngineStep.Stopped,
local: null,
remotes: this.remotes.map(() => null),
error: 'Sync progress manually stopped',
retrying: false,
};
}
@@ -209,10 +212,18 @@ export class SyncEngine {
updateSyncingState(local: SyncPeer | null, remotes: (SyncPeer | null)[]) {
let step = SyncEngineStep.Synced;
let error = null;
const allPeer = [local, ...remotes];
for (const peer of allPeer) {
if (!peer || peer.status.step !== SyncPeerStep.Synced) {
step = SyncEngineStep.Syncing;
if (peer && peer.status.step <= 0) {
// step < 0 means reject connection by server with some reason
// so the data may be out of date
step = SyncEngineStep.Rejected;
error = peer.status.lastError;
} else {
step = SyncEngineStep.Syncing;
}
break;
}
}
@@ -220,6 +231,7 @@ export class SyncEngine {
step,
local: local?.status ?? null,
remotes: remotes.map(peer => peer?.status ?? null),
error,
retrying: allPeer.some(
peer => peer?.status.step === SyncPeerStep.Retrying
),

View File

@@ -19,6 +19,7 @@ export interface SyncPeerStatus {
loadedDocs: number;
pendingPullUpdates: number;
pendingPushUpdates: number;
lastError: string | null;
}
/**
@@ -54,6 +55,7 @@ export class SyncPeer {
loadedDocs: 0,
pendingPullUpdates: 0,
pendingPushUpdates: 0,
lastError: null,
};
onStatusChange = new Slot<SyncPeerStatus>();
readonly abort = new AbortController();
@@ -119,6 +121,7 @@ export class SyncPeer {
loadedDocs: 0,
pendingPullUpdates: 0,
pendingPushUpdates: 0,
lastError: 'Retrying sync after 5 seconds',
};
await Promise.race([
new Promise<void>(resolve => {
@@ -199,6 +202,7 @@ export class SyncPeer {
abortInner.abort('subscribe disconnect:' + reason);
}
);
throwIfAborted(abortInner.signal);
// Step 1: load root doc
@@ -368,7 +372,11 @@ export class SyncPeer {
reportSyncStatus() {
let step;
if (this.state.connectedDocs.size === 0) {
let lastError = null;
if (this.storage.errorMessage?.type === 'outdated') {
step = SyncPeerStep.VersionRejected;
lastError = this.storage.errorMessage.message.reason;
} else if (this.state.connectedDocs.size === 0) {
step = SyncPeerStep.LoadingRootDoc;
} else if (this.state.subdocsLoadQueue.length || this.state.subdocLoading) {
step = SyncPeerStep.LoadingSubDoc;
@@ -391,6 +399,7 @@ export class SyncPeer {
this.state.pullUpdatesQueue.length + (this.state.subdocLoading ? 1 : 0),
pendingPushUpdates:
this.state.pushUpdatesQueue.length + (this.state.pushingUpdate ? 1 : 0),
lastError,
};
}

View File

@@ -1,9 +1,22 @@
export type RejectByVersion = {
currVersion: string;
requiredVersion: string;
reason: string;
};
export type SyncErrorMessage = {
type: 'outdated';
message: RejectByVersion;
};
export interface SyncStorage {
/**
* for debug
*/
name: string;
errorMessage?: SyncErrorMessage;
pull(
docId: string,
state: Uint8Array