// SPDX-License-Identifier: AGPL-3.0-or-later import {Logger} from '@app/features/platform/utils/AppLogger'; import {verifyRoundtripStability} from '@app/features/user/state/SyncedFieldRoundtrip'; import { createSyncedFieldMachineSnapshot, type SyncedFieldCommand, type SyncedFieldFailureReason, type SyncedFieldMachineEvent, selectSyncedFieldMachineModel, transitionSyncedFieldMachineSnapshot, } from '@app/features/user/state/SyncedFieldStateMachine'; import {create, equals, type Message, type MessageInitShape, toBinary} from '@bufbuild/protobuf'; import type {GenMessage} from '@bufbuild/protobuf/codegenv2'; import type {SyncedPreferences} from '@fluxer/schema/src/domains/user/SyncedPreferencesCodec'; import {comparer, reaction, runInAction} from 'mobx'; export {verifyRoundtripStability}; const logger = new Logger('SyncedField'); export type SyncedPreferencesField = Exclude; function isMessageSchema(value: unknown): value is GenMessage { return ( value != null && typeof value === 'object' && typeof (value as {typeName?: unknown}).typeName === 'string' && Array.isArray((value as {fields?: unknown}).fields) ); } export interface SyncedFieldConfig< T extends object, F extends SyncedPreferencesField, M extends Message & NonNullable, > { readonly field: F; readonly schema: GenMessage; readonly persist: ReadonlyArray; toMessage(store: T): MessageInitShape>; applyMessage(store: T, message: M): void; readonly version?: number; readonly debounceMs?: number; readonly syncAcrossTabs?: boolean; readonly maxEncodedBytes?: number; enabled?(): boolean; mergeRemote?(local: M, incoming: M): MessageInitShape>; } export async function makeSyncedField< T extends object, F extends SyncedPreferencesField, M extends Message & NonNullable, >(store: T, config: SyncedFieldConfig): Promise { const tag = `${store.constructor.name}.${String(config.field)}`; const schema = config.schema; if (!isMessageSchema(schema)) { logger.error(`${tag}: invalid protobuf schema; synced persistence disabled.`); return; } const isEnabled = (): boolean => config.enabled?.() ?? true; let machine = createSyncedFieldMachineSnapshot(); let ownerUserId: string | null = null; let observedUserId: string | null = null; const suspend = (reason: SyncedFieldFailureReason, message: string, error?: unknown): void => { logger.error(`${tag}: ${message}; synced persistence disabled:`, error); transitionMachine({ type: 'sync.failed', failure: { reason, message, error, }, }); }; const buildMessage = (): M => create(schema, config.toMessage(store)) as NonNullable as M; const buildMessageOrSuspend = (message: string): M | null => { if (!selectSyncedFieldMachineModel(machine).isActive) return null; try { return buildMessage(); } catch (error) { suspend('build-local', message, error); return null; } }; const getRemoteSnapshot = (): SyncedPreferences[F] | undefined => { if (!selectSyncedFieldMachineModel(machine).isActive) return undefined; if (!isEnabled()) return undefined; try { return UserSettings.getSubPreference(config.field); } catch (error) { suspend('read-remote', 'failed to read the remote snapshot', error); return undefined; } }; const compareRemote = (snapshot: M): void => { const candidate = buildMessageOrSuspend('failed to build the local snapshot before applying a remote update'); if (candidate == null) return; try { transitionOnly({type: equals(schema, snapshot, candidate) ? 'sync.remoteMatched' : 'sync.remoteNeedsApply'}); } catch (error) { suspend('compare-remote', 'failed to compare the remote snapshot with local state', error); } }; const applyRemote = (snapshot: M): void => { let effective = snapshot; if (config.mergeRemote && (ownerUserId === null || ownerUserId === observedUserId)) { try { effective = create(schema, config.mergeRemote(buildMessage(), snapshot)) as M; } catch (error) { logger.error(`${tag}: mergeRemote threw; applying the remote snapshot as-is:`, error); effective = snapshot; } } try { runInAction(() => { config.applyMessage(store, effective); }); } catch (error) { logger.error(`${tag}: applyMessage threw:`, error); } ownerUserId = observedUserId; transitionOnly({type: 'sync.applyFinished'}); }; const preparePush = (candidate: M): void => { ownerUserId ??= observedUserId; try { const current = UserSettings.getSubPreference(config.field); if (current !== undefined && equals(schema, current as M, candidate)) { transitionOnly({type: 'sync.localAlreadySynced'}); return; } const encodedBytes = toBinary(schema, candidate).length; if (encodedBytes > maxEncodedBytes) { logger.error( `${tag}: payload of ${encodedBytes} bytes exceeds per-field budget of ${maxEncodedBytes}; push dropped.`, ); transitionOnly({type: 'sync.localAlreadySynced'}); return; } const stability = verifyRoundtripStability({ schema, store, toMessage: config.toMessage, applyMessage: config.applyMessage, candidate, }); if (stability.threw !== undefined) { suspend('roundtrip-threw', 'applyMessage threw during roundtrip probe', stability.threw); return; } if (!stability.stable) { suspend( 'roundtrip-unstable', 'roundtrip-stability check failed (toMessage → applyMessage → toMessage produced a different message)', ); return; } } catch (error) { suspend('encode-local', 'failed to encode or validate the local snapshot', error); return; } transitionOnly({type: 'sync.localReadyToPush'}); }; const push = (candidate: M): void => { void UserSettings.setSubPreference(config.field, candidate).catch((error) => { const status = error != null && typeof error === 'object' && typeof (error as {status?: unknown}).status === 'number' ? ((error as {status: number}).status as number) : null; if (status === 429) return; logger.warn(`${tag}: failed to sync to server:`, error); }); transitionOnly({type: 'sync.commandHandled'}); }; const executeCommand = (command: SyncedFieldCommand): void => { switch (command.type) { case 'compareRemote': compareRemote(command.snapshot as M); return; case 'applyRemote': applyRemote(command.snapshot as M); return; case 'preparePush': preparePush(command.candidate as M); return; case 'pushLocal': push(command.candidate as M); return; } }; const processCommands = (): void => { for (let i = 0; i < 8; i++) { const command = selectSyncedFieldMachineModel(machine).command; if (command == null) return; executeCommand(command); } suspend('machine-loop', 'state machine command loop exceeded the safety limit'); }; function transitionOnly(event: SyncedFieldMachineEvent): void { machine = transitionSyncedFieldMachineSnapshot(machine, event); } function transitionMachine(event: SyncedFieldMachineEvent): void { transitionOnly(event); processCommands(); } await Promise.resolve(); const maxEncodedBytes = config.maxEncodedBytes ?? 65_536; const UserSettings = (await import('@app/features/user/state/UserSettings')).default; const SessionManager = (await import('@app/features/platform/state/AuthSession')).default; const applyDefaultsForUserChange = (userId: string): void => { try { runInAction(() => { config.applyMessage(store, create(schema) as M); }); } catch (error) { logger.error(`${tag}: applyMessage threw while resetting to defaults after an account change:`, error); } ownerUserId = userId; }; try { reaction( () => ({ snapshot: getRemoteSnapshot(), userId: SessionManager?.userId ?? null, hydrated: UserSettings.isHydrated(), }), ({snapshot, userId, hydrated}) => { observedUserId = userId; if ( snapshot === undefined && hydrated && userId != null && ownerUserId != null && ownerUserId !== userId && selectSyncedFieldMachineModel(machine).isActive ) { applyDefaultsForUserChange(userId); } transitionMachine({ type: 'sync.remoteObserved', enabled: isEnabled(), snapshot, }); }, {fireImmediately: true, equals: comparer.shallow}, ); reaction( () => (isEnabled() ? buildMessageOrSuspend('failed to build the local snapshot for observation') : null), (candidate) => { transitionMachine({ type: 'sync.localObserved', enabled: isEnabled(), candidate, }); }, config.debounceMs != null && config.debounceMs > 0 ? {delay: config.debounceMs} : undefined, ); } catch (error) { suspend('initialize-reactions', 'failed to initialize reactions', error); } }