Skip to content

Commit

Permalink
wip: add query cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Complexlity committed Aug 16, 2024
1 parent b487d43 commit e239b0b
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
testPlayground.mjs
testPlayground.ts
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(":")}`;

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
10 changes: 9 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,7 @@ export class neynarService implements Service {
async getUserByUsername(
username: string,
viewerFid?: number
): Promise<DataOrError<Omit<User, "powerBadge">>> {
): Promise<DataOrError<Omit<User, "powerBadge">>> {
try {
const usersInfo = await api.get<{ result: { user: any } }>(
"/v1/farcaster/user-by-username",
Expand Down Expand Up @@ -167,6 +174,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

0 comments on commit e239b0b

Please sign in to comment.