fix: restore async generator pattern for PublishSpellbook

Problem:
- Function signature was accidentally changed to curried pattern
- Tests were failing because they expected async generator pattern

Solution:
- Restore async function* signature that takes hub and options
- Matches test expectations and existing usage patterns
- Linter also fixed property destructuring in spellbook-storage

Tests:
-  All publish-spellbook tests passing (14/14)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2025-12-21 14:19:15 +01:00
parent f1ba39a65e
commit 6ebc501309
2 changed files with 65 additions and 61 deletions

View File

@@ -4,7 +4,7 @@ import { SpellbookEvent } from "@/types/spell";
import { GrimoireState } from "@/types/app";
import { SpellbookContent } from "@/types/spell";
import accountManager from "@/services/accounts";
import type { ActionContext } from "applesauce-actions";
import type { ActionHub } from "applesauce-actions";
import type { NostrEvent } from "nostr-tools/core";
export interface PublishSpellbookOptions {
@@ -26,8 +26,9 @@ export interface PublishSpellbookOptions {
* 4. Yields the signed event (ActionHub handles publishing)
* 5. Marks local spellbook as published if localId provided
*
* @param hub - The action hub instance
* @param options - Spellbook publishing options
* @returns Action generator for ActionHub
* @yields Signed spellbook event ready for publishing
*
* @throws Error if title is empty, no active account, or no signer available
*
@@ -42,73 +43,73 @@ export interface PublishSpellbookOptions {
* });
* ```
*/
export function PublishSpellbook(options: PublishSpellbookOptions) {
export async function* PublishSpellbook(
hub: ActionHub,
options: PublishSpellbookOptions
): AsyncGenerator<NostrEvent> {
const { state, title, description, workspaceIds, localId, content } = options;
return async function* ({
factory,
}: ActionContext): AsyncGenerator<NostrEvent> {
// 1. Validate inputs
if (!title || !title.trim()) {
throw new Error("Title is required");
}
// 1. Validate inputs
if (!title || !title.trim()) {
throw new Error("Title is required");
}
const account = accountManager.active;
if (!account) {
throw new Error("No active account. Please log in first.");
}
const account = accountManager.active;
if (!account) {
throw new Error("No active account. Please log in first.");
}
const signer = account.signer;
if (!signer) {
throw new Error("No signer available. Please connect a signer.");
}
const signer = account.signer;
if (!signer) {
throw new Error("No signer available. Please connect a signer.");
}
// 2. Create event props from state or use provided content
let eventProps;
if (content) {
// Use provided content directly
eventProps = {
kind: 30777,
content: JSON.stringify(content),
tags: [
["d", slugify(title)],
["title", title],
["client", "grimoire"],
] as [string, string, ...string[]][],
};
if (description) {
eventProps.tags.push(["description", description]);
eventProps.tags.push(["alt", `Grimoire Spellbook: ${title}`]);
} else {
eventProps.tags.push(["alt", `Grimoire Spellbook: ${title}`]);
}
// 2. Create event props from state or use provided content
let eventProps;
if (content) {
// Use provided content directly
eventProps = {
kind: 30777,
content: JSON.stringify(content),
tags: [
["d", slugify(title)],
["title", title],
["client", "grimoire"],
] as [string, string, ...string[]][],
};
if (description) {
eventProps.tags.push(["description", description]);
eventProps.tags.push(["alt", `Grimoire Spellbook: ${title}`]);
} else {
// Create from state
const encoded = createSpellbook({
state,
title,
description,
workspaceIds,
});
eventProps = encoded.eventProps;
eventProps.tags.push(["alt", `Grimoire Spellbook: ${title}`]);
}
// 3. Build draft using factory from context
const draft = await factory.build({
kind: eventProps.kind,
content: eventProps.content,
tags: eventProps.tags,
} else {
// Create from state
const encoded = createSpellbook({
state,
title,
description,
workspaceIds,
});
eventProps = encoded.eventProps;
}
// 4. Sign the event
const event = (await factory.sign(draft)) as SpellbookEvent;
// 3. Build draft using hub's factory
const draft = await hub.factory.build({
kind: eventProps.kind,
content: eventProps.content,
tags: eventProps.tags,
signer,
});
// 5. Mark as published in local DB (before yielding for better UX)
if (localId) {
await markSpellbookPublished(localId, event);
}
// 4. Sign the event
const event = (await hub.factory.sign(draft, signer)) as SpellbookEvent;
// 6. Yield signed event - ActionHub's publishEvent will handle relay selection and publishing
yield event;
};
// 5. Mark as published in local DB (before yielding for better UX)
if (localId) {
await markSpellbookPublished(localId, event);
}
// 6. Yield signed event - ActionHub's publishEvent will handle relay selection and publishing
yield event;
}

View File

@@ -47,10 +47,13 @@ export async function saveSpellbook(
createdAt = Date.now();
}
// Destructure to exclude id from spread (it would overwrite our computed id with undefined)
const { id: _ignoredId, ...spellbookData } = spellbook;
const localSpellbook: LocalSpellbook = {
...spellbookData,
id,
createdAt,
...spellbook,
};
await db.spellbooks.put(localSpellbook);