Skip to content

Commit

Permalink
feat: Add feature flag caching mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnPhamous committed Feb 14, 2025
1 parent 124c2f9 commit 8f36112
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 1 deletion.
79 changes: 79 additions & 0 deletions src/helpers/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { homedir } from "node:os";
import { join } from "node:path";

interface CachedFeatureFlag {
value: boolean;
expiresAt: number;
}

interface FeatureFlagCache {
[key: string]: CachedFeatureFlag;
}

const ONE_DAY_MS = 24 * 60 * 60 * 1000;

function getFeatureFlagCachePath(): string {
const configDir = join(homedir(), ".sfcompute");
return join(configDir, "feature-flags");
}

export async function saveFeatureFlags(flags: FeatureFlagCache): Promise<void> {
const cachePath = getFeatureFlagCachePath();
const configDir = join(homedir(), ".sfcompute");

try {
await Deno.mkdir(configDir, { recursive: true });
await Deno.writeTextFile(cachePath, JSON.stringify(flags, null, 2));
} catch (error) {
console.error("boba error saving feature flags:", error);
}
}

export async function loadFeatureFlags(): Promise<FeatureFlagCache> {
const cachePath = getFeatureFlagCachePath();

try {
const cacheData = await Deno.readTextFile(cachePath);
return JSON.parse(cacheData);
} catch {
return {};
}
}

export async function getCachedFeatureFlag(
feature: string,
accountId: string
): Promise<CachedFeatureFlag | null> {
const cache = await loadFeatureFlags();
const key = `${accountId}:${feature}`;
const cachedFlag = cache[key];

if (!cachedFlag) {
return null;
}

if (Date.now() > cachedFlag.expiresAt) {
// Cache expired, remove it
delete cache[key];
await saveFeatureFlags(cache);
return null;
}

return cachedFlag;
}

export async function cacheFeatureFlag(
feature: string,
accountId: string,
value: boolean
): Promise<void> {
const cache = await loadFeatureFlags();
const key = `${accountId}:${feature}`;

cache[key] = {
value,
expiresAt: Date.now() + ONE_DAY_MS,
};

await saveFeatureFlags(cache);
}
17 changes: 16 additions & 1 deletion src/lib/posthog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import process from "node:process";
import { PostHog } from "posthog-node";
import { loadConfig, saveConfig } from "../helpers/config.ts";
import {
cacheFeatureFlag,
getCachedFeatureFlag,
} from "../helpers/feature-flags.ts";
import { getApiUrl } from "../helpers/urls.ts";

const postHogClient = new PostHog(
Expand Down Expand Up @@ -76,12 +80,23 @@ export const isFeatureEnabled = async (feature: FeatureFlags) => {
return false;
}

// Check cache first
const cachedFlag = await getCachedFeatureFlag(feature, exchangeAccountId);
if (cachedFlag) {
return cachedFlag.value;
}

// If not in cache or expired, fetch from PostHog
const result = await postHogClient.isFeatureEnabled(
feature,
exchangeAccountId
);

return result;
// Cache the result (PostHog returns undefined if there's an error, default to false)
const finalResult = result ?? false;
await cacheFeatureFlag(feature, exchangeAccountId, finalResult);

return finalResult;
};

export const analytics = {
Expand Down
8 changes: 8 additions & 0 deletions src/types/deno.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare namespace Deno {
function writeTextFile(path: string, data: string): Promise<void>;
function readTextFile(path: string): Promise<string>;
function mkdir(
path: string,
options?: { recursive?: boolean }
): Promise<void>;
}

0 comments on commit 8f36112

Please sign in to comment.