Files
grimoire/PLAN-APPLESAUCE-V5.md
Alejandro b2b398b9fb docs: add applesauce v5 upgrade plan (#39)
* docs: add applesauce v5 upgrade plan

Comprehensive migration plan covering:
- Package updates (add applesauce-common, update to v5)
- EventFactory import migration (applesauce-factory → applesauce-core)
- Unified event loader setup
- ActionHub → ActionRunner migration
- useObservableMemo → use$ hook migration
- New features: casting system, encrypted content caching
- Documentation and skills updates needed

* feat: upgrade applesauce libraries to v5

Major upgrade from applesauce v4 to v5 with breaking changes:

Package updates:
- applesauce-core: ^4.0.0 → ^5.0.0
- applesauce-actions: ^4.0.0 → ^5.0.0
- applesauce-loaders: ^4.0.0 → ^5.0.0
- applesauce-react: ^4.0.0 → ^5.0.0
- applesauce-relay: ^4.0.0 → ^5.0.0
- applesauce-signers: ^4.0.0 → ^5.0.0
- applesauce-accounts: ^4.0.0 → ^5.0.0
- Added new applesauce-common: ^5.0.0 package

API migrations:
- EventFactory: applesauce-factory → applesauce-core/event-factory
- ActionHub → ActionRunner with async function pattern (not generators)
- useObservableMemo → use$ hook across all components
- Helper imports: article, highlight, threading, zap, comment, lists
  moved from applesauce-core to applesauce-common
- parseCoordinate → parseReplaceableAddress
- Subscription options: retries → reconnect
- getEventPointerFromETag now returns null instead of throwing

New features:
- Unified event loader via createEventLoaderForStore
- Updated loaders.ts to use v5 unified loader pattern

Documentation:
- Updated CLAUDE.md with v5 patterns and migration notes
- Updated applesauce-core skill for v5 changes
- Created new applesauce-common skill

Test fixes:
- Updated publish-spellbook.test.ts for v5 ActionRunner pattern
- Updated publish-spell.test.ts with eventStore mock
- Updated relay-selection.test.ts with valid test events
- Updated loaders.test.ts with valid 64-char hex event IDs
- Added createEventLoaderForStore mock

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 14:54:21 +01:00

12 KiB

Applesauce v5 Upgrade Plan for Grimoire

Overview

This plan outlines the migration from applesauce v4 to v5, covering breaking changes, new features to adopt, and documentation updates.

Phase 1: Package Updates & Import Migration

1.1 Update package.json Dependencies

Current versions:

"applesauce-accounts": "^4.1.0",
"applesauce-actions": "^4.0.0",
"applesauce-content": "^4.0.0",
"applesauce-core": "latest",
"applesauce-loaders": "^4.2.0",
"applesauce-react": "^4.0.0",
"applesauce-relay": "latest",
"applesauce-signers": "^4.1.0"

Target versions:

"applesauce-accounts": "^5.0.0",
"applesauce-actions": "^5.0.0",
"applesauce-common": "^5.0.0",  // NEW - required for v5
"applesauce-content": "^5.0.0",
"applesauce-core": "^5.0.0",
"applesauce-loaders": "^5.0.0",
"applesauce-react": "^5.0.0",
"applesauce-relay": "^5.0.0",
"applesauce-signers": "^5.0.0"

Remove:

"applesauce-factory": "..." // Removed in v5 - EventFactory now in applesauce-core

1.2 EventFactory Import Migration

File: src/services/hub.ts

// Before (v4)
import { EventFactory } from "applesauce-factory";

// After (v5)
import { EventFactory } from "applesauce-core/event-factory";

1.3 Helper Import Migration

Many helpers moved from applesauce-core/helpers to applesauce-common/helpers. Need to audit all imports:

Helpers that moved to applesauce-common/helpers:

  • Profile helpers: getDisplayName, getProfilePicture
  • Social graph: groupPubkeysByRelay, getSeenRelays
  • Threading: getNip10References, interpretThreadTags
  • Zaps: getZapAmount, getZapSender, isValidZap
  • Lists: FAVORITE_RELAYS_KIND, getListTags, getRelaysFromList
  • Article helpers: getArticleTitle, getArticleSummary, etc.
  • Highlight helpers: all getHighlight* functions
  • Comment helpers: getCommentReplyPointer

Files to update (from exploration):

  • src/lib/nostr-utils.ts
  • src/lib/event-title.ts
  • src/lib/nip34-helpers.ts
  • src/lib/nip-c0-helpers.ts
  • src/services/loaders.ts
  • src/services/relay-selection.ts
  • src/hooks/useProfile.ts
  • src/hooks/useNostrEvent.ts
  • src/components/nostr/RichText.tsx
  • All kind renderers in src/components/nostr/kinds/

Strategy: Use grep to find all applesauce-core/helpers imports and update to applesauce-common/helpers where appropriate. Some low-level helpers remain in applesauce-core/helpers.


Phase 2: Loader Migration (Unified Event Loader)

2.1 Current Loader Architecture

File: src/services/loaders.ts

Currently uses:

  • createEventLoader - for single events
  • createAddressLoader - for replaceable/addressable events
  • createTimelineLoader - for timelines
  • Custom eventLoader wrapper with smart relay hint merging

2.2 Unified Loader Setup

Replace separate loaders with unified loader:

// Before (v4)
import {
  createEventLoader,
  createAddressLoader,
  createTimelineLoader,
} from "applesauce-loaders/loaders";

const baseEventLoader = createEventLoader(pool, { eventStore, extraRelays });
export const addressLoader = createAddressLoader(pool, { eventStore, extraRelays });
export const profileLoader = createAddressLoader(pool, { eventStore, bufferTime: 200, extraRelays });

// After (v5)
import { createEventLoaderForStore } from "applesauce-loaders/loaders";

// One-time setup - attaches loader to eventStore
createEventLoaderForStore(eventStore, pool, {
  bufferTime: 200,
  followRelayHints: true,
  extraRelays: AGGREGATOR_RELAYS,
  lookupRelays: ["wss://purplepag.es/", "wss://index.hzrd149.com/"],
});

// Usage changes - use eventStore methods directly
eventStore.event({ id: "event_id" }).subscribe(...);
eventStore.replaceable({ kind: 0, pubkey: "pubkey" }).subscribe(...);
eventStore.addressable({ kind: 30023, pubkey, identifier }).subscribe(...);

2.3 Custom eventLoader Wrapper

The current smart relay hint merging in eventLoader() needs to be preserved. Options:

Option A: Keep custom wrapper, use unified loader internally

export function eventLoader(pointer, context) {
  // Keep existing relay hint merging logic
  const enhancedPointer = mergeRelayHints(pointer, context);

  // Use eventStore.event() from unified loader
  return eventStore.event(enhancedPointer);
}

Option B: Move relay hint logic to a cacheRequest/extraRelays callback

createEventLoaderForStore(eventStore, pool, {
  bufferTime: 200,
  followRelayHints: true,
  extraRelays: (pointer) => getRelayHintsForPointer(pointer),
});

Recommendation: Option A - Keep custom wrapper for backward compatibility and control over relay selection.

2.4 Hook Updates for Unified Loader

Files to update:

  • src/hooks/useNostrEvent.ts - Use eventStore.event() / eventStore.replaceable()
  • src/hooks/useProfile.ts - Use eventStore.replaceable() for kind 0
  • src/hooks/useTimeline.ts - May continue using createTimelineLoader
  • src/hooks/useLiveTimeline.ts - No change (uses pool.subscription directly)
  • src/hooks/useAccountSync.ts - Use eventStore.addressable() for kind 10002

Phase 3: Action System Migration (ActionHub → ActionRunner)

3.1 Breaking Changes

Before (v4): Actions are async generators that yield events

export function PublishSpellbook(options) {
  return async function* ({ factory }: ActionContext): AsyncGenerator<NostrEvent> {
    const draft = await factory.build({ kind, content, tags });
    const event = await factory.sign(draft);
    yield event; // ActionHub handles publishing
  };
}

After (v5): Actions are async functions that call context.publish()

export function PublishSpellbook(options) {
  return async function ({ factory, publish }: ActionContext): Promise<void> {
    const draft = await factory.build({ kind, content, tags });
    const event = await factory.sign(draft);
    await publish(event); // Explicit publish call
  };
}

3.2 Files to Migrate

  1. src/services/hub.ts:

    // Before
    import { ActionHub } from "applesauce-actions";
    export const hub = new ActionHub(eventStore, factory, publishEvent);
    
    // After
    import { ActionRunner } from "applesauce-actions";
    export const hub = new ActionRunner(eventStore, factory, publishEvent);
    
  2. src/actions/publish-spellbook.ts:

    • Convert from async generator to async function
    • Replace yield event with await publish(event)
  3. src/actions/publish-spell.ts:

    • Same conversion pattern
  4. src/actions/delete-event.ts:

    • Already uses direct factory.sign + pool.publish pattern
    • Consider migrating to ActionRunner for consistency

3.3 Action Context Changes

v5 ActionContext provides additional features:

  • publish(event, relays?) - Explicit publish with optional relay override
  • cast(event, Cast) - Cast events to typed classes
  • Sub-action support for composing actions

Phase 4: React Hooks Migration (use$ Hook)

4.1 New use$ Hook

Replace useObservableMemo with use$:

// Before (v4)
import { useObservableMemo } from "applesauce-react/hooks";

const event = useObservableMemo(() => eventStore.event(eventId), [eventId]);
const activeAccount = useObservableMemo(() => accounts.active$, []);

// After (v5)
import { use$ } from "applesauce-react/hooks";

const event = use$(() => eventStore.event(eventId), [eventId]);
const activeAccount = use$(accounts.active$); // Direct observable, no factory needed

4.2 Files to Update

Search for all useObservableMemo imports and update:

  • src/hooks/useNostrEvent.ts
  • src/hooks/useTimeline.ts
  • src/hooks/useLiveTimeline.ts
  • src/hooks/useAccountSync.ts
  • src/hooks/useStable.ts
  • Any component using useObservableMemo directly

4.3 Type Differences

use$ has better TypeScript inference:

  • BehaviorSubject<T> → returns T (never undefined)
  • Observable<T> → returns T | undefined

Phase 5: Adopt New Features

5.1 Casting System

Add casting for improved type safety and reactive patterns:

import { castEvent, Note, User, Profile } from "applesauce-common/casts";

// Cast events to typed classes
const note = castEvent(event, Note, eventStore);

// Synchronous properties
console.log(note.id, note.createdAt, note.isReply);

// Reactive observables
const profile = use$(note.author.profile$);
const replies = use$(note.replies$);

Potential use cases in Grimoire:

  • Note renderers - cast kind 1 events for cleaner access
  • Profile components - use User cast for profile data
  • Reply threads - use reactive replies$ observable
  • Zap displays - cast for amount/sender access

5.2 Encrypted Content Caching

For DM/encrypted content features:

import { persistEncryptedContent } from "applesauce-common/helpers";

// Setup in services/event-store.ts or main.tsx
persistEncryptedContent(eventStore, storage);

// Decrypted content automatically cached
await unlockHiddenBookmarks(bookmarks, signer);

5.3 Blueprints and Operations

Move event creation to blueprints for cleaner code:

// Before - manual event building
const draft = await factory.build({
  kind: 30777,
  content: JSON.stringify(content),
  tags: [["d", slug], ["title", title]]
});

// After - using blueprints (if available for kind 30777)
import { SpellbookBlueprint } from "applesauce-common/blueprints";
const draft = await factory.build(SpellbookBlueprint({ title, content, slug }));

Note: Custom kinds like 30777 may not have built-in blueprints, but the pattern can be used for standard kinds.


Phase 6: Documentation Updates

6.1 CLAUDE.md Updates

Update the following sections:

  1. Package structure - Add applesauce-common to stack description
  2. Import patterns - Update helper import paths
  3. Loader documentation - Document unified loader pattern
  4. Action documentation - Document ActionRunner pattern
  5. Hook documentation - Document use$ hook
  6. New features - Add casting system documentation
  7. Helper caching note - Update import paths in examples

6.2 Skill Updates

.claude/skills/applesauce-core/SKILL.md:

  • Update import paths for helpers that moved to applesauce-common
  • Add section on unified event loader
  • Update examples to use use$ hook
  • Add casting system documentation
  • Note v5 breaking changes

New skill: .claude/skills/applesauce-common/SKILL.md:

  • Document casting system (Note, User, Profile, etc.)
  • Document helpers that moved from applesauce-core
  • Document blueprints and operations
  • Document encrypted content caching

.claude/skills/applesauce-signers/SKILL.md:

  • Verify no breaking changes (appears stable)
  • Update if any signer interface changes

6.3 Code Comments

Update inline comments in affected files to reflect v5 patterns.


Phase 7: Testing & Verification

7.1 Test Updates

Update test files for new patterns:

  • src/actions/publish-spell.test.ts
  • src/actions/publish-spellbook.test.ts
  • Any tests using mocked applesauce imports

7.2 Verification Steps

  1. Run npm install after package.json updates
  2. Fix all TypeScript compilation errors
  3. Run npm run lint and fix issues
  4. Run npm run test:run and fix failing tests
  5. Run npm run build to verify production build
  6. Manual testing of critical flows:
    • Profile loading
    • Timeline feeds
    • Event detail views
    • Publishing events
    • Account login/logout

Migration Order

Recommended order to minimize breakage:

  1. Phase 1.1-1.3: Package updates and imports (required first)
  2. Phase 3: Action system migration (isolated change)
  3. Phase 2: Loader migration (larger change, test carefully)
  4. Phase 4: Hook migration (use$ is compatible alongside useObservableMemo)
  5. Phase 5: New features adoption (optional enhancements)
  6. Phase 6: Documentation updates
  7. Phase 7: Final testing and verification

Rollback Plan

If v5 migration encounters blocking issues:

  1. Revert package.json to v4 versions
  2. Run npm install to restore v4 packages
  3. Git revert any code changes
  4. Document blocking issues for resolution

Estimated Scope

Files to modify:

  • package.json - 1 file
  • Service files (src/services/) - 3-4 files
  • Hook files (src/hooks/) - 5-6 files
  • Action files (src/actions/) - 3 files
  • Helper usage across codebase - 20+ files
  • Documentation files - 4-5 files

New files:

  • .claude/skills/applesauce-common/SKILL.md

Risk areas:

  • Loader migration (most complex, affects data loading)
  • Helper import paths (many files, easy to miss some)
  • Action generator → async function (behavior change)