Skip to content

Commit 297bd54

Browse files
committed
add server status checking
1 parent 7cf47e3 commit 297bd54

File tree

5 files changed

+169
-17
lines changed

5 files changed

+169
-17
lines changed

src/sync/FridaySyncCore.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,13 @@ export class FridaySyncCore implements LiveSyncLocalDBEnv, LiveSyncCouchDBReplic
607607
async handleNetworkRecovery(): Promise<void> {
608608
Logger("Network recovery detected", LOG_LEVEL_INFO);
609609

610+
// Check current status - if already LIVE, skip recovery
611+
const currentStatus = this.replicationStat.value.syncStatus;
612+
if (currentStatus === "LIVE") {
613+
Logger("Sync already in LIVE state, skipping recovery", LOG_LEVEL_VERBOSE);
614+
return;
615+
}
616+
610617
// Update network status
611618
this._managers?.networkManager.setServerReachable(true);
612619

@@ -623,10 +630,9 @@ export class FridaySyncCore implements LiveSyncLocalDBEnv, LiveSyncCouchDBReplic
623630
// Restart sync if configured
624631
// Check if sync needs to be restarted - only skip if already in LIVE state
625632
if (this._settings.liveSync && this._replicator) {
626-
const status = this.replicationStat.value.syncStatus;
627-
// Restart if not in LIVE state (covers PAUSED, CLOSED, ERRORED, NOT_CONNECTED, etc.)
628-
if (status !== "LIVE") {
629-
Logger(`Restarting sync after network recovery (current status: ${status})`, LOG_LEVEL_INFO);
633+
// Only restart if not in LIVE state
634+
if (currentStatus !== "LIVE") {
635+
Logger(`Restarting sync after network recovery (current status: ${currentStatus})`, LOG_LEVEL_INFO);
630636
await this.startSync(true);
631637
}
632638
}

src/sync/core/common/messagesJson/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,5 +601,8 @@
601601
"fridaySync.error.cannotConnectServer": "Cannot connect to sync server. Your changes will be saved locally and synced when connection is restored.",
602602
"fridaySync.error.serverTimeout": "Connection to sync server timed out. Will retry automatically.",
603603
"fridaySync.status.offlineMode": "Offline mode - changes saved locally",
604-
"fridaySync.status.reconnecting": "Reconnecting to sync server..."
604+
"fridaySync.status.reconnecting": "Reconnecting to sync server...",
605+
"fridaySync.error.noCacheOffline": "First-time setup requires server connection",
606+
"fridaySync.error.firstTimeNeedsConnection": "First-time setup requires server connection. Please connect to network and restart sync.",
607+
"fridaySync.offline.workingMode": "Working offline - changes will sync when connected"
605608
}

src/sync/core/common/messagesJson/zh.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,5 +436,8 @@
436436
"fridaySync.error.cannotConnectServer": "无法连接同步服务器。您的修改将保存在本地,恢复连接后会自动同步。",
437437
"fridaySync.error.serverTimeout": "连接同步服务器超时。将自动重试。",
438438
"fridaySync.status.offlineMode": "离线模式 - 修改已保存到本地",
439-
"fridaySync.status.reconnecting": "正在重新连接同步服务器..."
439+
"fridaySync.status.reconnecting": "正在重新连接同步服务器...",
440+
"fridaySync.error.noCacheOffline": "首次使用需要连接服务器",
441+
"fridaySync.error.firstTimeNeedsConnection": "首次使用需要连接服务器。请连接网络后重新启动同步。",
442+
"fridaySync.offline.workingMode": "离线工作中 - 连接后自动同步"
440443
}

src/sync/core/replication/couchdb/LiveSyncReplicator.ts

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
SyncParamsUpdateError,
5858
} from "../SyncParamsHandler.ts";
5959
import type { ServiceHub } from "../../services/ServiceHub.ts";
60+
import { arrayBufferToBase64Single, base64ToArrayBufferInternalBrowser } from "../../string_and_binary/convert.ts";
6061

6162
const currentVersionRange: ChunkVersionRange = {
6263
min: 0,
@@ -203,17 +204,124 @@ export class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
203204
}
204205
}
205206

207+
// Instance-level salt cache (session lifetime)
208+
private _saltCache: Uint8Array | null = null;
209+
206210
override async getReplicationPBKDF2Salt(
207211
setting: RemoteDBSettings,
208-
refresh?: boolean
212+
refresh?: boolean = false
209213
): Promise<Uint8Array<ArrayBuffer>> {
210-
const server = `${setting.couchDB_URI}/${setting.couchDB_DBNAME}`;
211-
const manager = createSyncParamsHanderForServer(server, {
212-
put: (params: SyncParameters) => this.putSyncParameters(setting, params),
213-
get: () => this.getSyncParameters(setting),
214-
create: () => this.getInitialSyncParameters(setting),
215-
});
216-
return await manager.getPBKDF2Salt(refresh);
214+
215+
// ========== Step 1: Check instance cache ==========
216+
// Fast path: return cached salt if available and not forcing refresh
217+
if (this._saltCache && !refresh) {
218+
Logger("Using session salt cache", LOG_LEVEL_VERBOSE);
219+
return this._saltCache;
220+
}
221+
222+
// ========== Step 2: Check server reachability ==========
223+
const serverReachable = this.env.isServerReachable?.() ?? true;
224+
225+
if (!serverReachable) {
226+
// 🔑 Server unreachable: try local persistent cache
227+
// CRITICAL: Do NOT call manager.getPBKDF2Salt() to avoid _fetchSyncParameters
228+
Logger("Server unreachable, trying local salt cache", LOG_LEVEL_VERBOSE);
229+
230+
const cachedSalt = await this.getLocalCachedSalt(setting.couchDB_DBNAME);
231+
232+
if (cachedSalt) {
233+
Logger("Using local salt cache (offline mode)", LOG_LEVEL_INFO);
234+
this._saltCache = cachedSalt; // Update session cache
235+
return cachedSalt;
236+
}
237+
238+
// No cache available - cannot proceed offline
239+
Logger("No salt cache available for offline mode", LOG_LEVEL_INFO);
240+
throw new Error(
241+
$msg("fridaySync.error.noCacheOffline") ||
242+
"First-time setup requires server connection"
243+
);
244+
}
245+
246+
// ========== Step 3: Server reachable - fetch normally ==========
247+
try {
248+
const server = `${setting.couchDB_URI}/${setting.couchDB_DBNAME}`;
249+
const manager = createSyncParamsHanderForServer(server, {
250+
put: (params: SyncParameters) => this.putSyncParameters(setting, params),
251+
get: () => this.getSyncParameters(setting),
252+
create: () => this.getInitialSyncParameters(setting),
253+
});
254+
255+
// Fetch from server via SyncParamsHandler
256+
const salt = await manager.getPBKDF2Salt(refresh);
257+
258+
// ✅ Success: update both caches
259+
this._saltCache = salt; // Session cache
260+
await this.saveLocalSaltCache(setting.couchDB_DBNAME, salt); // Persistent cache
261+
262+
Logger("Salt fetched from server and cached", LOG_LEVEL_VERBOSE);
263+
return salt;
264+
265+
} catch (ex) {
266+
// Server fetch failed - try fallback to local cache
267+
Logger(`Failed to fetch salt from server: ${ex}`, LOG_LEVEL_VERBOSE);
268+
269+
const cachedSalt = await this.getLocalCachedSalt(setting.couchDB_DBNAME);
270+
271+
if (cachedSalt) {
272+
Logger("Falling back to local salt cache", LOG_LEVEL_INFO);
273+
this._saltCache = cachedSalt;
274+
return cachedSalt;
275+
}
276+
277+
// Real failure - no server and no cache
278+
Logger("No salt available (server failed and no cache)", LOG_LEVEL_INFO);
279+
throw ex;
280+
}
281+
}
282+
283+
/**
284+
* Save salt to local persistent cache (reuses existing "friday-sync-salt" store)
285+
*/
286+
private async saveLocalSaltCache(
287+
dbName: string,
288+
salt: Uint8Array
289+
): Promise<void> {
290+
try {
291+
const saltKey = this._getKnownSaltKey(dbName);
292+
const saltStore = this.env.services.database.openSimpleStore<string>("friday-sync-salt");
293+
const saltBase64 = await arrayBufferToBase64Single(salt);
294+
await saltStore.set(saltKey, saltBase64);
295+
Logger("Salt saved to local cache", LOG_LEVEL_VERBOSE);
296+
} catch (ex) {
297+
Logger(`Failed to save salt cache: ${ex}`, LOG_LEVEL_VERBOSE);
298+
// Non-critical error, don't throw
299+
}
300+
}
301+
302+
/**
303+
* Get salt from local persistent cache (reuses existing "friday-sync-salt" store)
304+
*/
305+
private async getLocalCachedSalt(
306+
dbName: string
307+
): Promise<Uint8Array | null> {
308+
try {
309+
const saltKey = this._getKnownSaltKey(dbName);
310+
const saltStore = this.env.services.database.openSimpleStore<string>("friday-sync-salt");
311+
const saltBase64 = await saltStore.get(saltKey);
312+
313+
if (saltBase64) {
314+
const salt = new Uint8Array(base64ToArrayBufferInternalBrowser(saltBase64));
315+
Logger("Local salt cache found", LOG_LEVEL_VERBOSE);
316+
return salt;
317+
}
318+
319+
Logger("No local salt cache found", LOG_LEVEL_VERBOSE);
320+
return null;
321+
} catch (ex) {
322+
Logger(`Failed to read salt cache: ${ex}`, LOG_LEVEL_VERBOSE);
323+
return null;
324+
}
217325
}
218326

219327
// eslint-disable-next-line require-await

src/sync/features/NetworkEvents/index.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,17 @@ export class FridayNetworkEvents {
7575
Logger(`Network status changed: ${isOnline ? "online" : "offline"}`, LOG_LEVEL_INFO);
7676

7777
if (isOnline) {
78+
// Check current sync status before attempting recovery
79+
const currentStatus = this.core.replicationStat.value.syncStatus;
80+
81+
if (currentStatus === "LIVE") {
82+
// Already in LIVE state, no need to reconnect
83+
Logger("Network online but sync already active", LOG_LEVEL_VERBOSE);
84+
return;
85+
}
86+
7887
// Network recovered - trigger reconnection
88+
Logger("Network recovered, attempting reconnection", LOG_LEVEL_INFO);
7989
await this.core.handleNetworkRecovery();
8090
} else {
8191
// Network lost - update status
@@ -121,9 +131,31 @@ export class FridayNetworkEvents {
121131
} else {
122132
// Window visible again
123133
if (!this.hasFocus) return;
124-
Logger("Window visible, checking for sync updates", LOG_LEVEL_VERBOSE);
125-
// Trigger a sync check on resume
126-
await this.core.handleNetworkRecovery();
134+
135+
// Check current sync status - only reconnect if needed
136+
const currentStatus = this.core.replicationStat.value.syncStatus;
137+
138+
// If already in LIVE state, no need to reconnect
139+
if (currentStatus === "LIVE") {
140+
Logger("Window visible, sync already active", LOG_LEVEL_VERBOSE);
141+
return;
142+
}
143+
144+
// If status is NOT_CONNECTED or ERRORED, attempt recovery
145+
if (currentStatus === "NOT_CONNECTED" || currentStatus === "ERRORED") {
146+
Logger("Window visible, checking for sync recovery", LOG_LEVEL_VERBOSE);
147+
148+
// Avoid duplicate reconnection attempts if already scheduled
149+
if (this.core.connectionMonitor?.isReconnectScheduled()) {
150+
Logger("Reconnect already scheduled, skipping duplicate attempt", LOG_LEVEL_VERBOSE);
151+
return;
152+
}
153+
154+
await this.core.handleNetworkRecovery();
155+
} else {
156+
// Other states (STARTED, PAUSED, CLOSED), just log
157+
Logger(`Window visible, current status: ${currentStatus}`, LOG_LEVEL_VERBOSE);
158+
}
127159
}
128160
}
129161

0 commit comments

Comments
 (0)