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 touseEventModel
- 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:
// 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:
// 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
- Replace TimelineLoader:
For code that is using the TimelineLoader
class, you can replace it with the createTimelineLoader
function:
// 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
});
- Replace SingleEventLoader:
For code that is using the SingleEventLoader
class, you can replace it with the createEventLoader
function:
// 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
});
- Replace ReplaceableLoader:
For code that is using the ReplaceableLoader
class, you can replace it with the createAddressLoader
function:
// 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:
// 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.
// 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.
// 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:
// 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:
// 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:
// 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
- Remove QueryStoreProvider:
// v1 - Remove
<QueryStoreProvider queryStore={queryStore}>
// v2 - Use EventStoreProvider
<EventStoreProvider eventStore={eventStore}>
- Update hook imports and usage:
// 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:
// 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
- Update stream subscriptions:
// 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:
// 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
setEventContentEncryptionMethod
→setEncryptedContentEncryptionMethod
:
// 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:
// 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:
// v2 - Returns boolean
const wasUpdated = eventStore.update(event);
if (wasUpdated) {
console.log("Event was updated successfully");
} else {
console.log("Event update failed");
}
Migration Steps
- Handle null returns from add():
// 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;
}
- Handle boolean returns from update():
// 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
- Replace all loader classes with functional loaders
- Rename all
Queries
toModels
in imports and usage - Remove QueryStore and use
eventStore.model()
directly - Update React hooks from
useStoreQuery
touseEventModel
- Replace QueryStoreProvider with
EventStoreProvider
- Update EventStore stream names from
inserts
/updates
/removes
toinsert$
/update$
/remove$
- Replace removed APIs like
getPointerFromTag
with specific pointer helpers - Rename methods like
setEventContentEncryptionMethod
→setEncryptedContentEncryptionMethod
- Handle new return values from
EventStore.add()
(can returnnull
) andEventStore.update()
(returns boolean)