@@ -57,6 +57,7 @@ import {
5757 SyncParamsUpdateError ,
5858} from "../SyncParamsHandler.ts" ;
5959import type { ServiceHub } from "../../services/ServiceHub.ts" ;
60+ import { arrayBufferToBase64Single , base64ToArrayBufferInternalBrowser } from "../../string_and_binary/convert.ts" ;
6061
6162const 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
0 commit comments