Skip to content

Commit

Permalink
add in memory cache for config
Browse files Browse the repository at this point in the history
  • Loading branch information
cstrnt committed Dec 6, 2023
1 parent 1058da5 commit f0ad4f3
Show file tree
Hide file tree
Showing 25 changed files with 249 additions and 98 deletions.
7 changes: 7 additions & 0 deletions apps/angular-example/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# angular-example

## 0.0.14

### Patch Changes

- @tryabby/angular@2.0.4
- @tryabby/devtools@5.0.0

## 0.0.13

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion apps/angular-example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "angular-example",
"version": "0.0.13",
"version": "0.0.14",
"private": true,
"scripts": {
"ng": "ng",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/pages/reference/http.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Returns the data for the given project.
}
```

### GET `/api/v1/data/:projectId.js`
### GET `/api/v1/data/:projectId/script.js`

Gives the same response as `/api/v1/data/:projectId`, but in JavaScript format.
You can use this endpoint to load the data in the browser using a `<script>` tag.
Expand Down
9 changes: 9 additions & 0 deletions apps/web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# web

## 0.2.32

### Patch Changes

- Updated dependencies
- @tryabby/core@5.1.2
- @tryabby/next@5.0.4
- @tryabby/devtools@5.0.0

## 0.2.31

### Patch Changes
Expand Down
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.2.31",
"version": "0.2.32",
"private": true,
"scripts": {
"build": "next build",
Expand All @@ -18,6 +18,7 @@
},
"dependencies": {
"@code-hike/mdx": "0.8.3-next.1",
"@databases/cache": "^1.0.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
Expand Down
1 change: 1 addition & 0 deletions apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ model ApiKey {

enum ApiRequestType {
GET_CONFIG
GET_CONFIG_SCRIPT
TRACK_VIEW
}

Expand Down
127 changes: 68 additions & 59 deletions apps/web/src/pages/api/v1/data/[projectId]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,73 @@ import { trackPlanOverage } from "lib/logsnag";
import { RequestCache } from "server/services/RequestCache";
import { transformFlagValue } from "lib/flags";
import { RequestService } from "server/services/RequestService";
import createCache from "server/common/memory-cache";

const incomingQuerySchema = z.object({
export const incomingQuerySchema = z.object({
projectId: z.string().transform((v) => v.replace(/.js$/, "")),
environment: z.string(),
});

const configCache = createCache<string, AbbyDataResponse>({
name: "configCache",
expireAfterMilliseconds: 1000,
});

export async function getAbbyResponseWithCache({
environment,
projectId,
}: z.infer<typeof incomingQuerySchema>) {
const cachedConfig = configCache.get(projectId);

if (cachedConfig) {
return cachedConfig;
}

const [tests, flags] = await Promise.all([
prisma.test.findMany({
where: {
projectId,
},
include: { options: true },
}),
prisma.featureFlagValue.findMany({
where: {
environment: {
name: environment,
projectId,
},
},
include: { flag: { select: { name: true, type: true } } },
}),
]);

const response = {
tests: tests.map((test) => ({
name: test.name,
weights: test.options.map((o) => o.chance.toNumber()),
})),
flags: flags
.filter(({ flag }) => flag.type === "BOOLEAN")
.map((flagValue) => {
return {
name: flagValue.flag.name,
value: transformFlagValue(flagValue.value, flagValue.flag.type),
};
}),
remoteConfig: flags
.filter(({ flag }) => flag.type !== "BOOLEAN")
.map((flagValue) => {
return {
name: flagValue.flag.name,
value: transformFlagValue(flagValue.value, flagValue.flag.type),
};
}),
} satisfies AbbyDataResponse;

configCache.set(projectId, response);
return response;
}

export default async function getWeightsHandler(
req: NextApiRequest,
res: NextApiResponse
Expand All @@ -35,6 +96,10 @@ export default async function getWeightsHandler(
const { projectId, environment } = querySchemaResult.data;

try {
const response = await getAbbyResponseWithCache({ projectId, environment });

res.send(response);

const { events, planLimits, plan, is80PercentOfLimit } =
await EventService.getEventsForCurrentPeriod(projectId);

Expand All @@ -46,70 +111,14 @@ export default async function getWeightsHandler(
return;
}

const [tests, flags] = await Promise.all([
prisma.test.findMany({
where: {
projectId,
},
include: { options: true },
}),
prisma.featureFlagValue.findMany({
where: {
environment: {
name: environment,
projectId,
},
},
include: { flag: { select: { name: true, type: true } } },
}),
]);

const response = {
tests: tests.map((test) => ({
name: test.name,
weights: test.options.map((o) => o.chance.toNumber()),
})),
flags: flags
.filter(({ flag }) => flag.type === "BOOLEAN")
.map((flagValue) => {
return {
name: flagValue.flag.name,
value: transformFlagValue(flagValue.value, flagValue.flag.type),
};
}),
remoteConfig: flags
.filter(({ flag }) => flag.type !== "BOOLEAN")
.map((flagValue) => {
return {
name: flagValue.flag.name,
value: transformFlagValue(flagValue.value, flagValue.flag.type),
};
}),
} satisfies AbbyDataResponse;

const duration = performance.now() - now;

if (
typeof req.query.projectId === "string" &&
req.query.projectId.endsWith(".js")
) {
const jsContent = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify(
response
)}`;

res.setHeader("Content-Type", "application/javascript");

res.send(jsContent);
}

res.json(response);

if (is80PercentOfLimit) {
await trackPlanOverage(projectId, plan, is80PercentOfLimit);
}

await RequestCache.increment(projectId);

const duration = performance.now() - now;

RequestService.storeRequest({
projectId,
type: "GET_CONFIG",
Expand Down
71 changes: 71 additions & 0 deletions apps/web/src/pages/api/v1/data/[projectId]/script.js.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { trackPlanOverage } from "lib/logsnag";
import { NextApiRequest, NextApiResponse } from "next";
import NextCors from "nextjs-cors";
import { EventService } from "server/services/EventService";
import { RequestCache } from "server/services/RequestCache";
import { RequestService } from "server/services/RequestService";
import { getAbbyResponseWithCache, incomingQuerySchema } from ".";
import { ABBY_WINDOW_KEY } from "@tryabby/core";

export default async function getScriptHandler(
req: NextApiRequest,
res: NextApiResponse
) {
const now = performance.now();

await NextCors(req, res, {
methods: ["GET"],
origin: "*",
optionsSuccessStatus: 200,
});

const querySchemaResult = incomingQuerySchema.safeParse(req.query);
if (!querySchemaResult.success) {
res.status(400).json({ error: "Invalid query" });
return;
}

const { projectId, environment } = querySchemaResult.data;

const { events, planLimits, plan, is80PercentOfLimit } =
await EventService.getEventsForCurrentPeriod(projectId);

if (events > planLimits.eventsPerMonth) {
res.status(429).json({ error: "Plan limit reached" });
// TODO: send email
// TODO: send email if 80% of limit reached
await trackPlanOverage(projectId, plan);
return;
}

try {
const response = await getAbbyResponseWithCache({ projectId, environment });

const jsContent = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify(response)}`;

res.setHeader("Content-Type", "application/javascript");

res.send(jsContent);

if (is80PercentOfLimit) {
await trackPlanOverage(projectId, plan, is80PercentOfLimit);
}

await RequestCache.increment(projectId);

const duration = performance.now() - now;

RequestService.storeRequest({
projectId,
type: "GET_CONFIG",
durationInMs: duration,
}).catch((e) => {
console.error("Unable to store request", e);
});

return;
} catch (e) {
console.error(e);
res.status(500).json({ error: "Internal server error" });
}
}
5 changes: 5 additions & 0 deletions apps/web/src/server/common/memory-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import createCacheRealm from "@databases/cache";

const { createCache } = createCacheRealm({ maximumSize: 10_000 });

export default createCache;
1 change: 0 additions & 1 deletion apps/web/src/server/services/RequestService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ApiRequest } from "@prisma/client";
import { hashString } from "utils/apiKey";
import { prisma } from "server/db/client";

export abstract class RequestService {
Expand Down
7 changes: 7 additions & 0 deletions packages/angular/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @tryabby/angular

## 2.0.4

### Patch Changes

- Updated dependencies
- @tryabby/core@5.1.2

## 2.0.3

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryabby/angular",
"version": "2.0.3",
"version": "2.0.4",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @tryabby/core

## 5.1.2

### Patch Changes

- add esm packages

## 5.1.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryabby/core",
"version": "5.1.1",
"version": "5.1.2",
"description": "",
"main": "dist/index.js",
"files": [
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ export class Abby<
window[ABBY_WINDOW_KEY] != null
) {
this.log(`loadProjectData() => using window data`);
this.init(window[ABBY_WINDOW_KEY] as AbbyDataResponse);
return;
return this.init(window[ABBY_WINDOW_KEY] as AbbyDataResponse);
}

const data = await HttpService.getProjectData({
Expand All @@ -181,7 +180,7 @@ export class Abby<
this.log(`loadProjectData() => no data`);
return;
}
this.init(data);
return this.init(data);
}

async getProjectDataAsync(): Promise<LocalData<FlagName, TestName, RemoteConfigName>> {
Expand Down
8 changes: 8 additions & 0 deletions packages/next/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @tryabby/next

## 5.0.4

### Patch Changes

- add esm packages
- Updated dependencies
- @tryabby/react@5.0.4

## 5.0.3

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryabby/next",
"version": "5.0.3",
"version": "5.0.4",
"description": "",
"main": "dist/index.js",
"files": [
Expand Down
Loading

2 comments on commit f0ad4f3

@vercel
Copy link

@vercel vercel bot commented on f0ad4f3 Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on f0ad4f3 Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.