mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor deleteAllVersions in NostrClient and fix delete flow test
- Extract logic from `deleteAllVersions` into helper methods: `_hydrateHistoryForDelete`, `_getEventsToDelete`, `_softDeleteVersions`, and `_hardDeleteVersions`.
- Update `_hardDeleteVersions` to use `{ waitForAll: true }` when publishing Kind 5 events, ensuring relay failures are correctly reported.
- Fix `tests/nostr-delete-flow.test.mjs` by explicitly registering the active signer in the test environment.
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
f0633910d6
commit
ac9eab9a10
2 files changed
+89
-45
No files matched your search
+82
-45
@@ -3081,43 +3081,19 @@ export class NostrClient {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versions of a video.
|
||||
*
|
||||
* **Process:**
|
||||
* 1. **Hydration**: Ensures the full edit history is loaded so we know all `d` tags and Event IDs.
|
||||
* 2. **Soft Delete (Revert)**: Publishes a new update with `deleted: true` for every unique `d` tag/root found.
|
||||
* This clears the content for clients that just resolve the "latest" version.
|
||||
* 3. **Hard Delete (NIP-09)**: Publishes a Kind 5 event referencing ALL known Event IDs (`e` tags) and
|
||||
* NIP-33 addresses (`a` tags). Relays compliant with NIP-09 will physically remove the events.
|
||||
* 4. **Tombstoning**: Updates local state to ensure the deleted video doesn't reappear from cache.
|
||||
*
|
||||
* @param {string} videoRootId - The root ID of the video series.
|
||||
* @param {string} pubkey - The owner's public key.
|
||||
* @param {{confirm?: boolean, video?: object}} [options] - Options (confirm dialog, target video hint).
|
||||
* @returns {Promise<{reverts: object[], deletes: object[]}|null>} Summary of actions taken, or null if cancelled.
|
||||
*/
|
||||
async deleteAllVersions(videoRootId, pubkey, options = {}) {
|
||||
if (!pubkey) {
|
||||
throw new Error("Not logged in to delete all versions.");
|
||||
}
|
||||
|
||||
const shouldConfirm = options?.confirm !== false;
|
||||
const targetVideo =
|
||||
options && typeof options.video === "object" ? options.video : null;
|
||||
let confirmed = true;
|
||||
|
||||
if (shouldConfirm && typeof window?.confirm === "function") {
|
||||
confirmed = window.confirm(
|
||||
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
||||
async _hydrateHistoryForDelete(targetVideo) {
|
||||
if (!targetVideo) return;
|
||||
try {
|
||||
await this.hydrateVideoHistory(targetVideo);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[nostr] Failed to hydrate video history before delete:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
devLogger.log("Deletion cancelled by user.");
|
||||
return null; // Cancel deletion if user clicks "Cancel"
|
||||
}
|
||||
|
||||
_getEventsToDelete(videoRootId, pubkey, targetVideo) {
|
||||
const normalizedPubkey = typeof pubkey === "string" ? pubkey.toLowerCase() : "";
|
||||
const normalizedRootInput =
|
||||
typeof videoRootId === "string" && videoRootId.trim().length
|
||||
@@ -3130,17 +3106,6 @@ export class NostrClient {
|
||||
: "");
|
||||
const targetDTag = this.resolveEventDTag(targetVideo);
|
||||
|
||||
if (targetVideo) {
|
||||
try {
|
||||
await this.hydrateVideoHistory(targetVideo);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[nostr] Failed to hydrate video history before delete:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const matchingEvents = new Map();
|
||||
for (const candidate of this.allEvents.values()) {
|
||||
if (!candidate || typeof candidate !== "object") {
|
||||
@@ -3183,6 +3148,10 @@ export class NostrClient {
|
||||
throw new Error("No existing events found for that root.");
|
||||
}
|
||||
|
||||
return { matchingEvents, inferredRoot };
|
||||
}
|
||||
|
||||
async _softDeleteVersions(matchingEvents, inferredRoot, targetVideo, pubkey) {
|
||||
const revertSummaries = [];
|
||||
const revertEvents = [];
|
||||
|
||||
@@ -3261,6 +3230,10 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
return { revertSummaries, revertEvents };
|
||||
}
|
||||
|
||||
async _hardDeleteVersions(matchingEvents, revertEvents, targetVideo, inferredRoot, pubkey) {
|
||||
const eventIdSet = new Set();
|
||||
const addressPointerSet = new Set();
|
||||
|
||||
@@ -3363,6 +3336,7 @@ export class NostrClient {
|
||||
this.pool,
|
||||
this.relays,
|
||||
signedDelete,
|
||||
{ waitForAll: true }
|
||||
);
|
||||
const publishSummary = summarizePublishResults(publishResults);
|
||||
|
||||
@@ -3405,6 +3379,69 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
return deleteSummaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versions of a video.
|
||||
*
|
||||
* **Process:**
|
||||
* 1. **Hydration**: Ensures the full edit history is loaded so we know all `d` tags and Event IDs.
|
||||
* 2. **Soft Delete (Revert)**: Publishes a new update with `deleted: true` for every unique `d` tag/root found.
|
||||
* This clears the content for clients that just resolve the "latest" version.
|
||||
* 3. **Hard Delete (NIP-09)**: Publishes a Kind 5 event referencing ALL known Event IDs (`e` tags) and
|
||||
* NIP-33 addresses (`a` tags). Relays compliant with NIP-09 will physically remove the events.
|
||||
* 4. **Tombstoning**: Updates local state to ensure the deleted video doesn't reappear from cache.
|
||||
*
|
||||
* @param {string} videoRootId - The root ID of the video series.
|
||||
* @param {string} pubkey - The owner's public key.
|
||||
* @param {{confirm?: boolean, video?: object}} [options] - Options (confirm dialog, target video hint).
|
||||
* @returns {Promise<{reverts: object[], deletes: object[]}|null>} Summary of actions taken, or null if cancelled.
|
||||
*/
|
||||
async deleteAllVersions(videoRootId, pubkey, options = {}) {
|
||||
if (!pubkey) {
|
||||
throw new Error("Not logged in to delete all versions.");
|
||||
}
|
||||
|
||||
const shouldConfirm = options?.confirm !== false;
|
||||
const targetVideo =
|
||||
options && typeof options.video === "object" ? options.video : null;
|
||||
let confirmed = true;
|
||||
|
||||
if (shouldConfirm && typeof window?.confirm === "function") {
|
||||
confirmed = window.confirm(
|
||||
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
||||
);
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
devLogger.log("Deletion cancelled by user.");
|
||||
return null; // Cancel deletion if user clicks "Cancel"
|
||||
}
|
||||
|
||||
await this._hydrateHistoryForDelete(targetVideo);
|
||||
|
||||
const { matchingEvents, inferredRoot } = this._getEventsToDelete(
|
||||
videoRootId,
|
||||
pubkey,
|
||||
targetVideo
|
||||
);
|
||||
|
||||
const { revertSummaries, revertEvents } = await this._softDeleteVersions(
|
||||
matchingEvents,
|
||||
inferredRoot,
|
||||
targetVideo,
|
||||
pubkey
|
||||
);
|
||||
|
||||
const deleteSummaries = await this._hardDeleteVersions(
|
||||
matchingEvents,
|
||||
revertEvents,
|
||||
targetVideo,
|
||||
inferredRoot,
|
||||
pubkey
|
||||
);
|
||||
|
||||
this.saveLocalData("delete-events", { immediate: true });
|
||||
|
||||
return {
|
||||
|
||||
@@ -37,8 +37,15 @@ await (async function testDeleteFlowPublishesDeletionFlag() {
|
||||
signedEvents.push(event);
|
||||
return { ...event, id: `signed-${signedEvents.length}` };
|
||||
},
|
||||
getPublicKey: async () => pubkey,
|
||||
};
|
||||
|
||||
client.signerManager.setActiveSigner({
|
||||
type: "extension",
|
||||
pubkey,
|
||||
signEvent: window.nostr.signEvent,
|
||||
});
|
||||
|
||||
client.ensureExtensionPermissions = async () => ({ ok: true });
|
||||
client.relays = ["wss://relay.ok", "wss://relay.fail"];
|
||||
client.rawEvents.set(baseVideo.id, {
|
||||
|
||||
Reference in new issue
Block a user