Skip to content

Commit db55849

Browse files
authored
Implemented basic flag system (#522)
no ref - Expected flags are declared on service initialisation - Flags are enabled via query string (i.e `GET /account?new-account-response=1`) - Header added to response to show which flags have been enabled
1 parent ff2637d commit db55849

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

src/app.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
} from './dispatchers';
8484
import { FeedUpdateService } from './feed/feed-update.service';
8585
import { FeedService } from './feed/feed.service';
86+
import { FlagService } from './flag/flag.service';
8687
import {
8788
createDerepostActionHandler,
8889
createFollowActionHandler,
@@ -246,6 +247,8 @@ if (process.env.MANUALLY_START_QUEUE === 'true') {
246247
});
247248
}
248249

250+
const flagService = new FlagService([]);
251+
249252
const events = new AsyncEvents();
250253
const fedifyContextFactory = new FedifyContextFactory();
251254

@@ -648,6 +651,26 @@ app.use(async (ctx, next) => {
648651
});
649652
});
650653

654+
app.use(async (ctx, next) => {
655+
return flagService.runInContext(async () => {
656+
const enabledFlags: string[] = [];
657+
658+
for (const flag of flagService.getRegistered()) {
659+
if (ctx.req.query(flag)) {
660+
flagService.enable(flag);
661+
662+
enabledFlags.push(flag);
663+
}
664+
}
665+
666+
if (enabledFlags.length > 0) {
667+
ctx.res.headers.set('x-enabled-flags', enabledFlags.join(','));
668+
}
669+
670+
return next();
671+
});
672+
});
673+
651674
function sleep(n: number) {
652675
return new Promise((resolve) => setTimeout(resolve, n));
653676
}

src/flag/flag.service.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { AsyncLocalStorage } from 'node:async_hooks';
2+
3+
export class FlagService {
4+
private flags: Set<string>;
5+
private store: AsyncLocalStorage<Set<string>>;
6+
7+
constructor(flags: string[]) {
8+
this.flags = new Set(flags);
9+
this.store = new AsyncLocalStorage<Set<string>>();
10+
}
11+
12+
public async runInContext<T>(fn: () => Promise<T>) {
13+
return this.store.run(new Set<string>(), fn);
14+
}
15+
16+
public enable(flag: string) {
17+
if (!this.flags.has(flag)) {
18+
return;
19+
}
20+
21+
const store = this.store.getStore();
22+
23+
if (!store) {
24+
return;
25+
}
26+
27+
store.add(flag);
28+
}
29+
30+
public isEnabled(flag: string) {
31+
if (!this.flags.has(flag)) {
32+
return false;
33+
}
34+
35+
const store = this.store.getStore();
36+
37+
if (!store) {
38+
return false;
39+
}
40+
41+
return store.has(flag);
42+
}
43+
44+
public getRegistered() {
45+
return Array.from(this.flags);
46+
}
47+
}

src/flag/flag.service.unit.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { FlagService } from './flag.service';
3+
4+
describe('FlagService', () => {
5+
it('should be able to register flags', () => {
6+
const flagService = new FlagService(['foo', 'bar']);
7+
8+
expect(flagService.getRegistered()).toEqual(['foo', 'bar']);
9+
});
10+
11+
it('should be able to enable a flag', () => {
12+
const flagService = new FlagService(['foo', 'bar']);
13+
14+
flagService.runInContext(async () => {
15+
flagService.enable('foo');
16+
17+
expect(flagService.isEnabled('foo')).toBe(true);
18+
expect(flagService.isEnabled('bar')).toBe(false);
19+
});
20+
});
21+
22+
it('should be able to enable multiple flags', () => {
23+
const flagService = new FlagService(['foo', 'bar']);
24+
25+
flagService.runInContext(async () => {
26+
flagService.enable('foo');
27+
flagService.enable('bar');
28+
29+
expect(flagService.isEnabled('foo')).toBe(true);
30+
expect(flagService.isEnabled('bar')).toBe(true);
31+
});
32+
});
33+
34+
it('should ignore unregistered flags', () => {
35+
const flagService = new FlagService(['foo']);
36+
37+
flagService.runInContext(async () => {
38+
flagService.enable('bar');
39+
40+
expect(flagService.isEnabled('bar')).toBe(false);
41+
});
42+
});
43+
44+
it('should isolate flags between different contexts', async () => {
45+
const flagService = new FlagService(['foo', 'bar']);
46+
47+
flagService.runInContext(async () => {
48+
flagService.enable('foo');
49+
50+
expect(flagService.isEnabled('foo')).toBe(true);
51+
expect(flagService.isEnabled('bar')).toBe(false);
52+
});
53+
54+
flagService.runInContext(async () => {
55+
flagService.enable('bar');
56+
57+
expect(flagService.isEnabled('foo')).toBe(false);
58+
expect(flagService.isEnabled('bar')).toBe(true);
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)