Skip to content

Migration Guide: v1 to v2

This guide will help you migrate your existing applesauce v1 applications to v2. Applesauce v2 introduces several breaking changes that improve the API consistency and performance, but require some code updates.

Overview of Major Changes

  • Loaders: All loader classes have been removed and replaced with functional loaders that return observables
  • Queries → Models: The "Queries" namespace has been renamed to "Models" throughout the codebase
  • QueryStore Removal: QueryStore has been merged into EventStore - use eventStore.model() instead
  • React Hooks: useStoreQuery has been renamed to useEventModel
  • EventStore API: Methods now return synchronous observables that emit undefined for missing events

1. Loader Changes

The Problem

In v1, loaders were classes that extended RxJS Subjects, making them stateful and harder to work with:

ts
// v1 - Class-based loaders
import { TimelineLoader, SingleEventLoader, ReplaceableLoader } from "applesauce-loaders/loaders";

const timelineLoader = new TimelineLoader(rxNostr, ...args);
const eventLoader = new SingleEventLoader(rxNostr, ...args);
const replaceableLoader = new ReplaceableLoader(rxNostr, ...args);

// The loaders had to be subscribe to globaly to work
replaceableLoader.subscribe((event) => eventStore.add(event));

// Request a profile
replaceableLoader.next({ kind: 0, pubkey });

// Then wait for the result
replaceableLoader.subscribe((event) => {
  if (event.kind === 0 && event.pubkey === pubkey) console.log(event);
});

This was difficult to work with since passing the request to the loader and subscribing to the results were two separate steps.

The Solution

In v2, loaders are functions that return observables, making them stateless and more functional:

ts
// v2 - Functional loaders
import { createTimelineLoader, createEventLoader, createAddressLoader } from "applesauce-loaders/loaders";

// Create loader functions
const timeline = createTimelineLoader(pool, { eventStore });
const eventLoader = createEventLoader(pool, { eventStore });
const addressLoader = createAddressLoader(pool, { eventStore });

// Request a single event
const event = await eventLoader({ id: "event-id" }).subscribe((evetn) => {
  console.log(event);
});

// Request a profile
const profile = await addressLoader({ kind: 0, pubkey }).subscribe((events) => {
  console.log(events);
});

// Load next page on a timeline
const timeline = await timeline().subscribe((events) => {

This allows the loaders to be used in a more functional way, since each request returns an observable and only returns the data for that single request.

Migration Steps

  1. Replace TimelineLoader:

For code that is using the TimelineLoader class, you can replace it with the createTimelineLoader function:

ts
// v1
const loader = new TimelineLoader(rxNostr, ...opts);

// Start the loader
loader.subscribe((events) => {
  // Handle events
});

// Request the next page of the timeline
loader.next();

// v2
const timelineLoader = createTimelineLoader(
  // A RelayPool instance or request method
  pool,
  // Relays to load from
  ["wss://relay.example.com"],
  // Filters for the timeline
  [{ kinds: [1], limit: 20 }],
  {
    // EventStore instance for deduplication
    eventStore,
    // Any other options
    ...opts,
  },
);

// Request the next page of the timeline
timelineLoader().subscribe((events) => {
  // Handle events
});
  1. Replace SingleEventLoader:

For code that is using the SingleEventLoader class, you can replace it with the createEventLoader function:

ts
// v1
const loader = new SingleEventLoader(rxNostr, ...opts);
loader.subscribe((event) => {
  // Handle event
});
loader.next("event-id");

// v2
const eventLoader = createEventLoader(pool, { eventStore });
eventLoader({ id: "event-id", relays: ["wss://relay.example.com"] }).subscribe((event) => {
  // Handle event
});
  1. Replace ReplaceableLoader:

For code that is using the ReplaceableLoader class, you can replace it with the createAddressLoader function:

ts
// v1
const loader = new ReplaceableLoader(rxNostr, ...opts);
loader.subscribe((event) => {
  // Handle event
});
loader.next({ kind: 0, pubkey });

// v2
const addressLoader = createAddressLoader(pool, { eventStore });
addressLoader({ kind: 0, pubkey, relays: ["wss://relay.example.com"] }).subscribe((events) => {
  // Handle events
});

New Loader Namespace

All loaders are now exported under the Loaders namespace:

ts
// v2 - Import from namespace
import { Loaders } from "applesauce-loaders";

const timelineLoader = Loaders.timelineLoader(pool, options);
const eventLoader = Loaders.eventPointerLoader(pool, options);
const addressLoader = Loaders.addressPointerLoader(pool, options);

2. Queries to Models

In v2, the Queries namespace has been renamed to Models throughout the codebase.

They are still functionally the same, but the names have been changed for clarity. and to allow the query store to be removed.

ts
// v1
import { Queries } from "applesauce-core";
// or import individual models
import { ProfileQuery, TimelineQuery } from "applesauce-core/queries";

const query = queryStore.createQuery(Queries.ProfileQuery, pubkey);
const timeline = queryStore.createQuery(Queries.TimelineQuery, { kinds: [1] });

// v2
import { Models } from "applesauce-core";
// or import individual models
import { ProfileModel, TimelineModel } from "applesauce-core/models";

const profile = eventStore.model(Models.ProfileModel, pubkey);
const timeline = eventStore.model(Models.TimelineModel, { kinds: [1] });

3. QueryStore Removal

The Problem

In v1, the QueryStore was seperate from the EventStore. This made things difficult since apps would need to manage two separate stores.

ts
// v1
import { EventStore, QueryStore, Queries } from "applesauce-core";

const eventStore = new EventStore();
const queryStore = new QueryStore(eventStore);

const profile = queryStore.createQuery(Queries.ProfileQuery, pubkey);

The Solution

In v2, QueryStore has been merged into EventStore. Use eventStore.model() directly:

ts
// v2
import { EventStore, Models } from "applesauce-core";

const eventStore = new EventStore();

const profile = eventStore.model(Models.ProfileModel, pubkey);

4. React Integration Changes

The Problem

In v1, React integration required QueryStore providers and specific hooks:

tsx
// v1
import { QueryStoreProvider } from "applesauce-react/providers";
import { useStoreQuery } from "applesauce-react/hooks";
import { Queries } from "applesauce-core";

function App() {
  const queryStore = new QueryStore(eventStore);

  return (
    <QueryStoreProvider queryStore={queryStore}>
      <Profile pubkey={pubkey} />
    </QueryStoreProvider>
  );
}

function Profile({ pubkey }) {
  const profile = useStoreQuery(Queries.ProfileQuery, [pubkey]);
  return <div>{profile?.name}</div>;
}

The Solution

In v2, use EventStore directly with the renamed hook:

tsx
// v2
import { EventStoreProvider } from "applesauce-react/providers";
import { useEventModel } from "applesauce-react/hooks";
import { Models } from "applesauce-core";

function App() {
  return (
    <EventStoreProvider eventStore={eventStore}>
      <Profile pubkey={pubkey} />
    </EventStoreProvider>
  );
}

function Profile({ pubkey }) {
  const profile = useEventModel(Models.ProfileModel, [pubkey]);
  return <div>{profile?.name}</div>;
}

Migration Steps

  1. Remove QueryStoreProvider:
tsx
// v1 - Remove
<QueryStoreProvider queryStore={queryStore}>

// v2 - Use EventStoreProvider
<EventStoreProvider eventStore={eventStore}>
  1. Update hook imports and usage:
tsx
// v1
import { useStoreQuery } from "applesauce-react/hooks";
const profile = useStoreQuery(Queries.ProfileQuery, pubkey);

// v2
import { useEventModel } from "applesauce-react/hooks";
const profile = useEventModel(Models.ProfileModel, pubkey);

5. EventStore Stream Changes

The Problem

In v1, EventStore streams used inconsistent naming without the RxJS convention. In v2, all EventStore streams follow RxJS naming conventions with $ suffix:

ts
// v1 - Inconsistent stream naming
eventStore.inserts.subscribe((event) => {
  console.log("New event:", event);
});

eventStore.updates.subscribe((event) => {
  console.log("Updated event:", event);
});

eventStore.removes.subscribe((event) => {
  console.log("Removed event:", event);
});

// v2 - Consistent stream naming with $ suffix
eventStore.insert$.subscribe((event) => {
  console.log("New event:", event);
});

eventStore.update$.subscribe((event) => {
  console.log("Updated event:", event);
});

eventStore.remove$.subscribe((event) => {
  console.log("Removed event:", event);
});

Migration Steps

  1. Update stream subscriptions:
ts
// v1
eventStore.inserts.subscribe(handler);
eventStore.updates.subscribe(handler);
eventStore.removes.subscribe(handler);

// v2
eventStore.insert$.subscribe(handler);
eventStore.update$.subscribe(handler);
eventStore.remove$.subscribe(handler);

6. Removed APIs

Several APIs have been removed in v2. Here's how to handle them:

Removed Methods

  • getPointerFromTag: This method has been removed. Use the individual pointer helper functions instead:
ts
// v1
import { getPointerFromTag } from "applesauce-core";
const pointer = getPointerFromTag(tag);

// v2 - Use specific pointer helpers
import { getEventPointerFromTag, getAddressPointerFromTag, getPubkeyPointerFromTag } from "applesauce-core/helpers";

const eventPointer = getEventPointerFromTag(tag);
const addressPointer = getAddressPointerFromTag(tag);
const pubkeyPointer = getPubkeyPointerFromTag(tag);

Renamed Methods

  • setEventContentEncryptionMethodsetEncryptedContentEncryptionMethod:
ts
// v1
import { setEventContentEncryptionMethod } from "applesauce-core";
setEventContentEncryptionMethod(kind, "nip04");

// v2
import { setEncryptedContentEncryptionMethod } from "applesauce-core";
setEncryptedContentEncryptionMethod(kind, "nip04");

7. EventStore Return Value Changes

EventStore.add() Changes

The EventStore.add() method now has more specific return behavior:

ts
// v2 - Updated return values
const result = eventStore.add(event);

if (result === null) {
  // Event was ignored by verifyEvent function
  console.log("Event was rejected");
} else if (result === event) {
  // Event was successfully added as new
  console.log("New event added");
} else {
  // Event was a duplicate, existing event returned
  console.log("Duplicate event, existing instance returned");
}

EventStore.update() Changes

The EventStore.update() method now returns a boolean:

ts
// v2 - Returns boolean
const wasUpdated = eventStore.update(event);
if (wasUpdated) {
  console.log("Event was updated successfully");
} else {
  console.log("Event update failed");
}

Migration Steps

  1. Handle null returns from add():
ts
// v1
const addedEvent = eventStore.add(event);
// addedEvent was always the event or existing event

// v2
const result = eventStore.add(event);
if (result === null) {
  // Handle rejected event
  console.warn("Event was rejected by verification");
} else {
  // Use the returned event (could be existing duplicate)
  const addedEvent = result;
}
  1. Handle boolean returns from update():
ts
// v1
eventStore.update(event); // void return

// v2
const success = eventStore.update(event);
if (!success) {
  console.warn("Failed to update event");
}

8. Additional Changes

Summary of Required Changes

  1. Replace all loader classes with functional loaders
  2. Rename all Queries to Models in imports and usage
  3. Remove QueryStore and use eventStore.model() directly
  4. Update React hooks from useStoreQuery to useEventModel
  5. Replace QueryStoreProvider with EventStoreProvider
  6. Update EventStore stream names from inserts/updates/removes to insert$/update$/remove$
  7. Replace removed APIs like getPointerFromTag with specific pointer helpers
  8. Rename methods like setEventContentEncryptionMethodsetEncryptedContentEncryptionMethod
  9. Handle new return values from EventStore.add() (can return null) and EventStore.update() (returns boolean)