Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: add query cache #13

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Cast, DataOrError, User, UserWithOptionalViewerContext } from "./types";

export type StringOrNumberArray = (string | number)[];

type MapData<T> = {
data:T
timestamp: number;
}

export class Cache {
private cache = new Map<string, MapData<User | UserWithOptionalViewerContext | Cast>>();
private ttl: number = 60 * 60 * 1000;
constructor(config: CacheConfig = {}) {
if (config.ttl) {
this.ttl = config.ttl;
}
}

private getData(key: string): User | UserWithOptionalViewerContext | Cast | null {
const cachedData = this.cache.get(key);
if (cachedData) {
return cachedData.data;
}
return null;
}

private getUserCachedData(params: StringOrNumberArray): User |UserWithOptionalViewerContext | null {
const cacheKey = `${["user", ...params].join(":")}`;
return this.getData(cacheKey) as User |UserWithOptionalViewerContext | null;
}

private getCastCachedData(params: StringOrNumberArray): Cast
| null {
const cacheKey = `${["cast", ...params].join(":")}`;
console.log({getCastCacheKey: cacheKey})
return this.getData(cacheKey) as Cast | null;
}

private setUserCachedData(data: User, params: StringOrNumberArray): User {
const fidKey = `${["user", data.fid, ...params].join(":")}`;
const usernameKey = `${["user", data.username, ...params].join(":")}`;
const setData = { data, timestamp: Date.now() };
this.cache.set(fidKey, setData);
this.cache.set(usernameKey, setData);
return data
}
private setCastCachedData(data: Cast, params: StringOrNumberArray): Cast {
const hashKey = `${["cast", data.hash, ...params].join(":")}`;
const urlKey = `${["cast", data.url, ...params].join(":")}`;
const setData = { data, timestamp: Date.now() };
this.cache.set(hashKey, setData);
this.cache.set(urlKey, setData);
return data
}

public get<T extends CacheKeys>(type: T, params: StringOrNumberArray) {

if (type === "user") {
return this.getUserCachedData(params);
}
if (type === "cast") {
return this.getCastCachedData(params);
}
//Add more cache types if needed
throw new Error("Invalid cache type");
}

public set<T extends CacheKeys>(
type: T,
data: CacheTypes[T],
params: StringOrNumberArray
): User | Cast {
if (type === "user") {
return this.setUserCachedData(data as User, params);
}
if (type === "cast") {
return this.setCastCachedData(data as Cast, params);
}
//Add more cache types if needed
throw new Error("Invalid cache type");
}
}

type CacheConfig = {
ttl?: number;
};

export interface CacheTypes {
user: User | UserWithOptionalViewerContext;
cast: Cast;
}
export type CacheKeys = keyof CacheTypes;

type P = CacheTypes[CacheKeys]
107 changes: 74 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Cast, Config, DataOrError, Service } from "@/types";
import { Cast, Config, DataOrError, Service, User, UserWithOptionalViewerContext } from "@/types";
import { services, TService } from "@/services";
import { isAddress } from "@/utils";
import { DEFAULTS } from "@/constants";
import { Logger, LogLevel, Noop } from "@/logger";
import { Cache, CacheKeys, CacheTypes, StringOrNumberArray } from "@/cache";

class uniFarcasterSdk implements Omit<Service, "name"> {
private neynarApiKey: string | undefined;
Expand All @@ -11,18 +12,52 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
private activeService: Service | undefined;
private debug: boolean = false;
private logLevel: LogLevel | undefined;
private cache: Cache = new Cache();
constructor(config: Config) {
const { activeServiceName, neynarApiKey, airstackApiKey, debug, logLevel } =
evaluateConfig(config);
const {
activeServiceName,
neynarApiKey,
airstackApiKey,
debug,
logLevel,
cacheTtl,
} = evaluateConfig(config);
this.neynarApiKey = neynarApiKey;
this.airstackApiKey = airstackApiKey;
this.activeService = this.createService(activeServiceName);
this.logLevel = logLevel;
this.debug = debug;
if (cacheTtl) {
this.cache = new Cache({ ttl: cacheTtl });
}
}

private async withCache<T extends CacheKeys, >(
type: T,
fn: (...args: any[]) => Promise<DataOrError<CacheTypes[T]>>,
params: StringOrNumberArray
) {
const cachedData = this.cache.get(type, params);
if (cachedData) {
this.logger({ name: "Cache Hit" }).success(`${params.join(" ")}`)
return {
data: cachedData as CacheTypes[T],
error: null
}
}
this.logger({ name: "Cache Miss" }).error(`${params.join(" ")}`);
const result = await fn.apply(this.activeService, params);
const { data, error } = result;
if (data) {
//First params is the fid or username and we don't want to add that since we would get that from data
const setParams = params.slice(1)
this.cache.set(type, data, setParams);
}
return result;
}

private logger(service: { name: string } = { name: "Main" }) {
if(!this.debug) return new Noop();
if (!this.debug) return new Noop();
return new Logger(service.name, this.logLevel);
}

Expand Down Expand Up @@ -50,18 +85,21 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
}

public setActiveService(service: TService) {
this.logger().info(`setting Active to: ${service}`);
this.logger().info(`setting Active to: ${service}`);
this.activeService = this.createService(service);
this.logger().info(`active service: ${this.activeService!.name}`);
this.logger().info(`active service: ${this.activeService!.name}`);
}

public async getUserByFid(fid: number, viewerFid: number = DEFAULTS.fid) {
this.logger(this.activeService!).info(
`fetching user by fid: ${fid} ${
viewerFid ? `and viewerFid: ${viewerFid}` : ""
}`
);
const res = await this.activeService!.getUserByFid(fid, viewerFid);
this.logger(this.activeService!).info(
`fetching user by fid: ${fid} ${
viewerFid ? `and viewerFid: ${viewerFid}` : ""
}`
);
const res = await this.withCache("user", this.activeService!.getUserByFid, [
fid,
viewerFid,
]) as DataOrError<User>;
if (this.debug && res.error) {
this.logger(this.activeService!).error(
`failed to fetch user by fid: ${fid} ${
Expand All @@ -82,21 +120,17 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
username: string,
viewerFid: number = DEFAULTS.fid
) {

this.logger(this.activeService!).info(
`fetching user by username: ${username} ${
viewerFid ? `and viewerFid: ${viewerFid}` : ""
}`
this.logger(this.activeService!).info(
`fetching user by username: ${username} ${
viewerFid ? `and viewerFid: ${viewerFid}` : ""
}`
);
if (this.activeService!.name === "airstack") {
this.logger(this.activeService!).warning(
"viewer context is not returned when fetching by username with airstack. Fetch by fid instead or use neynar service"
);
if (this.activeService!.name === "airstack") {
this.logger(this.activeService!).warning(
"viewer context is not returned when fetching by username with airstack. Fetch by fid instead or use neynar service"
);
}
const res = await this.activeService!.getUserByUsername(
username,
viewerFid
);
const res = await this.withCache("user", this.activeService!.getUserByUsername, [username, viewerFid])
if (res.error) {
this.logger(this.activeService!).error(
`Failed to fetch user by username: ${username} ${
Expand All @@ -118,7 +152,9 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
let res: DataOrError<Cast>;
if (!isValidHash) {
res = { data: null, error: { message: "Invalid hash" } };
} else res = await this.activeService!.getCastByHash(hash, viewerFid);
} else {
res = await this.withCache("cast", this.activeService!.getCastByHash, [hash, viewerFid]);
}
if (res.error) {
this.logger(this.activeService!).error(
`failed to fetch cast by hash: ${hash} ${
Expand All @@ -136,12 +172,12 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
}

public async getCastByUrl(url: string, viewerFid: number = DEFAULTS.fid) {
this.logger(this.activeService!).info(
`fetching cast by url: ${url} ${
viewerFid ? `and viewerFid: ${viewerFid}` : ""
}`
);
const res = await this.activeService!.getCastByUrl(url, viewerFid);
this.logger(this.activeService!).info(
`fetching cast by url: ${url} ${
viewerFid ? `and viewerFid: ${viewerFid}` : ""
}`
);
const res = await this.withCache("cast", this.activeService!.getCastByUrl, [url, viewerFid]);
if (res.error) {
this.logger(this.activeService!).error(
`failed to fetch cast by url: ${url} ${
Expand All @@ -165,6 +201,7 @@ function evaluateConfig(config: Config) {
let neynarApiKey: string | undefined;
let debug: boolean = false;
let logLevel: LogLevel | undefined = undefined;
let cacheTtl: number | undefined = undefined;
if (
"neynarApiKey" in config &&
"airstackApiKey" in config &&
Expand Down Expand Up @@ -200,8 +237,12 @@ function evaluateConfig(config: Config) {
}
}
}
if ("cacheTtl" in config) {
cacheTtl = config.cacheTtl;
}

return { activeServiceName, neynarApiKey, airstackApiKey, debug, logLevel };
return { activeServiceName, neynarApiKey, airstackApiKey, debug, logLevel, cacheTtl };
}

export default uniFarcasterSdk;
export * from './cache'
2 changes: 2 additions & 0 deletions src/services/airstack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export class airstackService implements Service {

private getCastFromAirstackResult(castResult: AirstackCastQueryResult) {
const convertedCast: Cast = {
hash: castResult.FarcasterCasts.Cast[0]!.hash,
url: castResult.FarcasterCasts.Cast[0]!.url,
author: this.getUserFromAirstackSociaslResult(
castResult.FarcasterCasts.Cast[0]!.castedBy
),
Expand Down
4 changes: 3 additions & 1 deletion src/services/airstack/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export interface Cast {
channel: Channel;
numberOfLikes: number;
numberOfRecasts: number;
castedBy: CastedBy;
castedBy: CastedBy;
url: string,
hash: string
}

export interface CastedBy {
Expand Down
2 changes: 2 additions & 0 deletions src/services/airstack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const socialReturnedQuery = `
isFarcasterPowerUser`;

const castReturnedQuery = `
url
hash
embeds
text
channel {
Expand Down
11 changes: 10 additions & 1 deletion src/services/neynar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,16 @@ export class neynarService implements Service {
};
}

private makeCastUrlFromHash(username: string, hash: string) {
return `https://warpcast.com/${username}/${hash.slice(0, 10)}`;
}


private getCastFromNeynarResponse(cast: NeynarCast): Cast {
return {
author: this.getUserFromNeynarResponse(cast.author),
hash: cast.hash,
url: this.makeCastUrlFromHash(cast.author.username, cast.hash),
userReactions: {
likes: cast.reactions.likes_count,
recasts: cast.reactions.recasts_count,
Expand Down Expand Up @@ -107,7 +114,8 @@ export class neynarService implements Service {
async getUserByUsername(
username: string,
viewerFid?: number
): Promise<DataOrError<Omit<User, "powerBadge">>> {
): Promise<DataOrError<Omit<User, "powerBadge">>> {
console.log(username, viewerFid)
try {
const usersInfo = await api.get<{ result: { user: any } }>(
"/v1/farcaster/user-by-username",
Expand Down Expand Up @@ -167,6 +175,7 @@ export class neynarService implements Service {

const cast = castInfo.data.cast;
const returnedCast = this.getCastFromNeynarResponse(cast);
returnedCast.url = url;
return { data: returnedCast, error: null };
} catch (e) {
return this.handleError(e);
Expand Down
28 changes: 20 additions & 8 deletions src/testPlayground.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import uniFarcasterSdk from "@";
import uniFarcasterSdk, { Cache } from "../dist/index.mjs";

const sdk = new uniFarcasterSdk({
neynarApiKey: "NEYNAR_API_DOCS",
airstackApiKey: "1534c9578b8e645b796727f8d3236993b",
airstackApiKey: "AIRSTACK_API_DOCS",
activeService: "neynar",
debug: true,
logLevel: "warning"
debug: true,
});

sdk.getActiveService();
// sdk.getActiveService();
// const {data: user, error} = await sdk.getUserByUsername('noseals');
// const { data: user2, error2 } = await sdk.getUserByUsername('noseals');
// sdk.setActiveService('neynar');
// const { data: user3, error3 } = await sdk.getUserByUsername('noseals');
// const { data: user4, error4 } = await sdk.getUserByUsername(user3.fid);

const { data: cast, error: errorCast } = await sdk.getCastByHash(
"0xbed8c16b624d656b3471746f0d73ab9d8ec1fb95"
);
sdk.setActiveService("airstack");
const user = await sdk.getUserByFid(213144);
// sdk.setActiveService("neynar");
const user2 = await sdk.getUserByUsername("v")
const { data: cast1, error: errorCast1 } = await sdk.getCastByUrl(
cast.url
);
const { data: cast2, error: errorCast2 } = await sdk.getCastByHash(
cast.hash
);

Loading
Loading