mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[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:
1 parent
a2c9f44307
commit
023f37f08f
3 files changed
+172
-196
No files matched your search
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user