[sync] Unify NWC + Storage sync onto one shared implementation (step 1: backend)

walletSyncService and storageSyncService were near-identical copies (the "two
sync systems"). Extract the shared machinery into createEncryptedSyncItem() —
per-pubkey enable flags, conflict-checked push, cheap remote-exists check,
enable/disable/pull — so each service is now a thin config differing only in
d-tag, kind, availability, and buildPayload/applyPayload. Public surface
(isAvailable/isEnabled/hasRemote/push/pull/enable/disable + the *_DTAG export)
preserved; both dedicated suites pass (8).

Next (step 2): shared confirm dialog + consistent notifications + seamless
auto-sync in the two profile controllers (the user-facing UX unification).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-01 15:49:42 -04:00
1 parent a2c9f44307
commit 023f37f08f
3 files changed
+172 -196

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
// One encrypted cross-login sync implementation, shared by every syncable item
// (NWC wallet, storage credentials, …). Previously walletSyncService and
// storageSyncService were near-identical copies; the only per-item differences
// are the d-tag, the sync "kind", the availability check, and how the payload is
// built (push) / applied (pull). This factory captures the shared machinery —
// per-pubkey enable flags, conflict-checked push, cheap remote existence check,
// enable/disable — so the two "systems" become one.
//
// Item config:
// id "wallet" | "storage" — the per-pubkey sync-flag kind
// dtag stable d-tag for the encrypted note
// encryptedSync the encrypted-sync facade
// available () => boolean (extra availability beyond encryptedSync)
// buildPayload async (pubkey) => payload | null (null => "nothing-to-sync")
// applyPayload async (pubkey, payload) => void | { imported:false, error }
// logLabel short label for warnings
import { encryptedSync as defaultEncryptedSync } from "../nostr/encryptedSyncFacade.js";
import { isSyncEnabled, setSyncEnabled } from "./settingsSyncFlags.js";
import { pushWithConflictCheck } from "./syncConflict.js";
import { userLogger } from "../utils/logger.js";
export function createEncryptedSyncItem({
id,
dtag,
encryptedSync = defaultEncryptedSync,
available = () => true,
buildPayload,
applyPayload,
logLabel = id || "sync",
} = {}) {
function normalizePubkey(pubkey) {
return typeof pubkey === "string" ? pubkey.trim() : "";
}
function isAvailable() {
return Boolean(encryptedSync?.isAvailable?.()) && Boolean(available());
}
function isEnabled(pubkey) {
return isSyncEnabled(normalizePubkey(pubkey), id);
}
function setEnabledFlag(pubkey, enabled) {
setSyncEnabled(normalizePubkey(pubkey), id, enabled);
}
// Cheap existence check (no decrypt) for the login-restore offer.
async function hasRemote() {
if (typeof encryptedSync.exists !== "function") {
return false;
}
const result = await encryptedSync.exists(dtag);
return Boolean(result?.exists);
}
async function push(pubkey, { confirmOverwrite } = {}) {
const key = normalizePubkey(pubkey);
if (!key) {
return { ok: false, error: "missing-pubkey" };
}
let payload;
try {
payload = await buildPayload(key);
} catch (error) {
userLogger.warn(`[${logLabel}] Failed to build sync payload:`, error);
return { ok: false, error: "export-failed", cause: error };
}
if (!payload) {
return { ok: false, error: "nothing-to-sync" };
}
return pushWithConflictCheck({
encryptedSync,
dTag: dtag,
kind: id,
pubkey: key,
payload,
confirmOverwrite,
});
}
async function pull(pubkey) {
const key = normalizePubkey(pubkey);
if (!key) {
return { found: false, error: "missing-pubkey" };
}
const result = await encryptedSync.pull(dtag);
if (!result?.found) {
return result;
}
try {
const applied = await applyPayload(key, result.payload || {});
if (applied && applied.imported === false) {
return { ...result, imported: false, error: applied.error };
}
} catch (error) {
userLogger.warn(`[${logLabel}] Failed to apply pulled settings:`, error);
return { ...result, imported: false, error: "import-failed", cause: error };
}
return { ...result, imported: true };
}
async function enable(pubkey, options = {}) {
setEnabledFlag(pubkey, true);
return push(pubkey, options);
}
async function disable(pubkey) {
setEnabledFlag(pubkey, false);
return encryptedSync.clear(dtag);
}
return {
id,
dtag,
isAvailable,
isEnabled,
hasRemote,
push,
pull,
enable,
disable,
};
}
export default createEncryptedSyncItem;
+17 -96
View File
@@ -9,9 +9,7 @@
import { encryptedSync as defaultEncryptedSync } from "../nostr/encryptedSyncFacade.js";
import defaultStorageService from "./storageService.js";
import { isSyncEnabled, setSyncEnabled } from "./settingsSyncFlags.js";
import { pushWithConflictCheck } from "./syncConflict.js";
import { userLogger } from "../utils/logger.js";
import { createEncryptedSyncItem } from "./encryptedSyncItem.js";
export const STORAGE_SYNC_DTAG = "bitvid:storage-connections";
const SYNC_KIND = "storage";
@@ -20,100 +18,23 @@ export function createStorageSyncService({
encryptedSync = defaultEncryptedSync,
storage = defaultStorageService,
} = {}) {
function normalizePubkey(pubkey) {
return typeof pubkey === "string" ? pubkey.trim() : "";
}
const item = createEncryptedSyncItem({
id: SYNC_KIND,
dtag: STORAGE_SYNC_DTAG,
encryptedSync,
logLabel: "storageSync",
// Export the local storage account record (self-protected). null => nothing
// to sync. An export error surfaces as { error: "export-failed" }.
buildPayload: async (pubkey) => {
const record = await storage.exportAccountRecord(pubkey);
return record || null;
},
applyPayload: async (pubkey, payload) => {
await storage.importAccountRecord(pubkey, payload);
},
});
function isAvailable() {
return Boolean(encryptedSync?.isAvailable?.());
}
function isEnabled(pubkey) {
return isSyncEnabled(normalizePubkey(pubkey), SYNC_KIND);
}
function setEnabledFlag(pubkey, enabled) {
setSyncEnabled(normalizePubkey(pubkey), SYNC_KIND, enabled);
}
// Cheap check (no decrypt) for whether a synced note exists — for the
// login-restore prompt.
async function hasRemote() {
if (typeof encryptedSync.exists !== "function") {
return false;
}
const result = await encryptedSync.exists(STORAGE_SYNC_DTAG);
return Boolean(result?.exists);
}
// Push the current local storage account record to the user's encrypted note.
async function push(pubkey, { confirmOverwrite } = {}) {
const key = normalizePubkey(pubkey);
if (!key) {
return { ok: false, error: "missing-pubkey" };
}
let record;
try {
record = await storage.exportAccountRecord(key);
} catch (error) {
userLogger.warn("[storageSync] Failed to export storage record:", error);
return { ok: false, error: "export-failed", cause: error };
}
if (!record) {
return { ok: false, error: "nothing-to-sync" };
}
return pushWithConflictCheck({
encryptedSync,
dTag: STORAGE_SYNC_DTAG,
kind: SYNC_KIND,
pubkey: key,
payload: record,
confirmOverwrite,
});
}
// Pull the encrypted note and import it into local storage. Returns the
// encrypted-sync pull result with `imported: true` when a record was applied.
async function pull(pubkey) {
const key = normalizePubkey(pubkey);
if (!key) {
return { found: false, error: "missing-pubkey" };
}
const result = await encryptedSync.pull(STORAGE_SYNC_DTAG);
if (!result?.found) {
return result;
}
try {
await storage.importAccountRecord(key, result.payload);
} catch (error) {
userLogger.warn("[storageSync] Failed to import storage record:", error);
return { ...result, imported: false, error: "import-failed", cause: error };
}
return { ...result, imported: true };
}
// Turn sync on: remember the choice and push immediately.
async function enable(pubkey, options = {}) {
setEnabledFlag(pubkey, true);
return push(pubkey, options);
}
// Turn sync off: remember the choice and wipe the published note.
async function disable(pubkey) {
setEnabledFlag(pubkey, false);
return encryptedSync.clear(STORAGE_SYNC_DTAG);
}
return {
STORAGE_SYNC_DTAG,
isAvailable,
isEnabled,
hasRemote,
push,
pull,
enable,
disable,
};
return { STORAGE_SYNC_DTAG, ...item };
}
export const storageSyncService = createStorageSyncService();
+29 -100
View File
@@ -11,9 +11,7 @@
// factory — the caller injects the live service.
import { encryptedSync as defaultEncryptedSync } from "../nostr/encryptedSyncFacade.js";
import { isSyncEnabled, setSyncEnabled } from "./settingsSyncFlags.js";
import { pushWithConflictCheck } from "./syncConflict.js";
import { userLogger } from "../utils/logger.js";
import { createEncryptedSyncItem } from "./encryptedSyncItem.js";
export const WALLET_SYNC_DTAG = "bitvid:nwc";
const SYNC_KIND = "wallet";
@@ -22,109 +20,40 @@ export function createWalletSyncService({
encryptedSync = defaultEncryptedSync,
nwcSettings,
} = {}) {
function normalizePubkey(pubkey) {
return typeof pubkey === "string" ? pubkey.trim() : "";
}
function isAvailable() {
return Boolean(encryptedSync?.isAvailable?.()) && Boolean(nwcSettings);
}
function isEnabled(pubkey) {
return isSyncEnabled(normalizePubkey(pubkey), SYNC_KIND);
}
function setEnabledFlag(pubkey, enabled) {
setSyncEnabled(normalizePubkey(pubkey), SYNC_KIND, enabled);
}
// Build the minimal syncable payload from current wallet settings.
function buildPayload() {
const settings = nwcSettings?.getActiveNwcSettings?.() || {};
const nwcUri = typeof settings.nwcUri === "string" ? settings.nwcUri.trim() : "";
const payload = { nwcUri };
if (settings.defaultZap !== undefined) {
payload.defaultZap = settings.defaultZap;
}
return payload;
}
// Cheap check (no decrypt) for whether a synced note exists — for the
// login-restore prompt.
async function hasRemote() {
if (typeof encryptedSync.exists !== "function") {
return false;
}
const result = await encryptedSync.exists(WALLET_SYNC_DTAG);
return Boolean(result?.exists);
}
async function push(pubkey, { confirmOverwrite } = {}) {
const key = normalizePubkey(pubkey);
if (!key) {
return { ok: false, error: "missing-pubkey" };
}
const payload = buildPayload();
if (!payload.nwcUri) {
return { ok: false, error: "nothing-to-sync" };
}
return pushWithConflictCheck({
encryptedSync,
dTag: WALLET_SYNC_DTAG,
kind: SYNC_KIND,
pubkey: key,
payload,
confirmOverwrite,
});
}
async function pull(pubkey) {
const key = normalizePubkey(pubkey);
if (!key) {
return { found: false, error: "missing-pubkey" };
}
const result = await encryptedSync.pull(WALLET_SYNC_DTAG);
if (!result?.found) {
return result;
}
const payload = result.payload || {};
const nwcUri = typeof payload.nwcUri === "string" ? payload.nwcUri.trim() : "";
if (!nwcUri) {
return { ...result, imported: false, error: "empty-payload" };
}
try {
const item = createEncryptedSyncItem({
id: SYNC_KIND,
dtag: WALLET_SYNC_DTAG,
encryptedSync,
logLabel: "walletSync",
available: () => Boolean(nwcSettings),
// Minimal syncable payload from current wallet settings. null => nothing to
// sync (no wallet connected).
buildPayload: () => {
const settings = nwcSettings?.getActiveNwcSettings?.() || {};
const nwcUri = typeof settings.nwcUri === "string" ? settings.nwcUri.trim() : "";
if (!nwcUri) {
return null;
}
const payload = { nwcUri };
if (settings.defaultZap !== undefined) {
payload.defaultZap = settings.defaultZap;
}
return payload;
},
applyPayload: async (pubkey, payload) => {
const nwcUri = typeof payload.nwcUri === "string" ? payload.nwcUri.trim() : "";
if (!nwcUri) {
return { imported: false, error: "empty-payload" };
}
const partial = { nwcUri };
if (payload.defaultZap !== undefined) {
partial.defaultZap = payload.defaultZap;
}
await nwcSettings.updateActiveNwcSettings(partial);
} catch (error) {
userLogger.warn("[walletSync] Failed to apply pulled wallet settings:", error);
return { ...result, imported: false, error: "import-failed", cause: error };
}
return { ...result, imported: true };
}
},
});
async function enable(pubkey, options = {}) {
setEnabledFlag(pubkey, true);
return push(pubkey, options);
}
async function disable(pubkey) {
setEnabledFlag(pubkey, false);
return encryptedSync.clear(WALLET_SYNC_DTAG);
}
return {
WALLET_SYNC_DTAG,
isAvailable,
isEnabled,
hasRemote,
push,
pull,
enable,
disable,
};
return { WALLET_SYNC_DTAG, ...item };
}
export default createWalletSyncService;