@frames.js/debugger
Version:
Debug your Frames.js applications
220 lines (185 loc) • 5.08 kB
text/typescript
import type {
NotificationsNamespace,
RecordedEvent,
StorageInterface,
} from "../types";
import { Redis } from "@upstash/redis";
import crypto from "node:crypto";
import { NOTIFICATION_TTL_IN_SECONDS } from "../../constants";
import type { FrameNotificationDetails } from "@farcaster/frame-sdk";
let storageInstance: RedisNotificationsStorage | null = null;
export async function getStorage(
token: string,
url: string,
serverUrl: string
) {
if (!storageInstance) {
const redis = new Redis({ token, url });
storageInstance = new RedisNotificationsStorage(redis, serverUrl);
}
return storageInstance;
}
export class RedisNotificationsStorage implements StorageInterface {
constructor(
private redis: Redis,
private serverUrl: string
) {}
async registerNamespace(
id: string,
params: {
fid: number;
frameAppUrl: string;
webhookUrl: string;
}
) {
const namespace: NotificationsNamespace = {
id,
fid: params.fid,
frameAppUrl: params.frameAppUrl,
namespaceUrl: new URL(`/notifications/${id}`, this.serverUrl).toString(),
webhookUrl: params.webhookUrl,
frame: {
status: "removed",
},
};
await this.redis.set<NotificationsNamespace>(
createNamespaceKey(id),
namespace,
{
ex: NOTIFICATION_TTL_IN_SECONDS,
}
);
return namespace;
}
async getNamespace(id: string): Promise<NotificationsNamespace | null> {
const namespace = await this.redis.get<NotificationsNamespace>(
createNamespaceKey(id)
);
if (!namespace) {
return null;
}
await this.redis.expire(
createNamespaceKey(id),
NOTIFICATION_TTL_IN_SECONDS
);
return namespace;
}
async addFrame(namespace: NotificationsNamespace) {
if (namespace.frame.status === "added") {
throw new Error("Frame is already added");
}
const token = crypto.randomUUID();
const url = new URL(
`/notifications/${namespace.id}/send`,
this.serverUrl
).toString();
const notificationDetails: FrameNotificationDetails = {
url,
token,
};
await this.redis.set<NotificationsNamespace>(
createNamespaceKey(namespace.id),
{
...namespace,
frame: {
status: "added",
notificationDetails,
},
},
{
ex: NOTIFICATION_TTL_IN_SECONDS,
}
);
return notificationDetails;
}
async removeFrame(namespace: NotificationsNamespace) {
if (namespace.frame.status === "removed") {
throw new Error("Frame is already removed");
}
await this.redis.set<NotificationsNamespace>(
createNamespaceKey(namespace.id),
{
...namespace,
frame: {
status: "removed",
},
},
{
ex: NOTIFICATION_TTL_IN_SECONDS,
}
);
}
async enableNotifications(namespace: NotificationsNamespace) {
if (namespace.frame.status === "removed") {
throw new Error("Frame is not added");
}
const token = crypto.randomUUID();
const url = new URL(
`/notifications/${namespace.id}/send`,
this.serverUrl
).toString();
const notificationDetails: FrameNotificationDetails = {
token,
url,
};
await this.redis.set<NotificationsNamespace>(
createNamespaceKey(namespace.id),
{
...namespace,
frame: {
status: "added",
notificationDetails,
},
},
{
ex: NOTIFICATION_TTL_IN_SECONDS,
}
);
return notificationDetails;
}
async disableNotifications(namespace: NotificationsNamespace) {
if (namespace.frame.status === "removed") {
throw new Error("Frame is not added");
}
await this.redis.set<NotificationsNamespace>(
createNamespaceKey(namespace.id),
{
...namespace,
frame: {
status: "added",
notificationDetails: null,
},
},
{
ex: NOTIFICATION_TTL_IN_SECONDS,
}
);
}
async recordEvent(namespaceId: string, event: RecordedEvent): Promise<void> {
const eventListStorageKey = createEventListStorageKey(namespaceId);
await this.redis
.pipeline()
.rpush<RecordedEvent>(eventListStorageKey, event)
.expire(eventListStorageKey, NOTIFICATION_TTL_IN_SECONDS)
.exec();
return;
}
async listEvents(namespaceId: string): Promise<RecordedEvent[]> {
const namespace = await this.getNamespace(namespaceId);
if (!namespace || namespace.frame.status === "removed") {
return [];
}
const eventListStorageKey = createEventListStorageKey(namespaceId);
const notifications = await this.redis.lpop<RecordedEvent[]>(
eventListStorageKey,
100
);
return notifications || [];
}
}
function createEventListStorageKey(namespaceId: string) {
return `notifications-namespace:${namespaceId}:events`;
}
function createNamespaceKey(namespaceId: string) {
return `notifications-namespace:${namespaceId}`;
}