feat(backend-flags,backend-telemetry): create shared flag registry + telemetry buffer packages

- @bytelyst/backend-flags: createFlagRegistry() with defaults, 6 tests
- @bytelyst/backend-telemetry: createTelemetryBuffer() with enabled switch, 5 tests
This commit is contained in:
saravanakumardb1 2026-03-20 08:02:17 -07:00
parent 08661bbd97
commit 0d8c0a5ffe
9 changed files with 267 additions and 0 deletions

View File

@ -0,0 +1,27 @@
{
"name": "@bytelyst/backend-flags",
"version": "0.1.0",
"description": "In-memory feature flag registry for Fastify product backends",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^5.7.3",
"vitest": "^3.0.5"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest';
import { createFlagRegistry } from './index.js';
describe('createFlagRegistry', () => {
it('returns default flag values', () => {
const registry = createFlagRegistry({
defaults: { 'feature.a': true, 'feature.b': false },
});
expect(registry.isFeatureEnabled('feature.a')).toBe(true);
expect(registry.isFeatureEnabled('feature.b')).toBe(false);
});
it('returns false for unknown flags', () => {
const registry = createFlagRegistry({ defaults: {} });
expect(registry.isFeatureEnabled('nonexistent')).toBe(false);
});
it('getAllFlags returns all defaults', () => {
const registry = createFlagRegistry({
defaults: { a: true, b: false, c: true },
});
expect(registry.getAllFlags()).toEqual({ a: true, b: false, c: true });
});
it('setFlag overrides a value', () => {
const registry = createFlagRegistry({ defaults: { x: false } });
expect(registry.isFeatureEnabled('x')).toBe(false);
registry.setFlag('x', true);
expect(registry.isFeatureEnabled('x')).toBe(true);
});
it('setFlag creates new flags', () => {
const registry = createFlagRegistry({ defaults: {} });
registry.setFlag('new.flag', true);
expect(registry.isFeatureEnabled('new.flag')).toBe(true);
expect(registry.getAllFlags()).toEqual({ 'new.flag': true });
});
it('accepts userId parameter without error', () => {
const registry = createFlagRegistry({ defaults: { a: true } });
expect(registry.isFeatureEnabled('a', 'user-1')).toBe(true);
});
});

View File

@ -0,0 +1,38 @@
/**
* In-memory feature flag registry for product backends.
*
* Products call createFlagRegistry() with their default flags,
* then use isFeatureEnabled/getAllFlags/setFlag as needed.
*/
export interface FlagRegistry {
isFeatureEnabled(flag: string, userId?: string): boolean;
getAllFlags(): Record<string, boolean>;
setFlag(flag: string, value: boolean): void;
}
export interface FlagRegistryOptions {
/** Default flag values. */
defaults: Record<string, boolean>;
/** Master switch when false, flags are still resolved from defaults but
* the registry won't attempt remote/dynamic flag resolution (future use). */
enabled?: boolean;
}
export function createFlagRegistry(opts: FlagRegistryOptions): FlagRegistry {
const flags: Map<string, boolean> = new Map(Object.entries(opts.defaults));
return {
isFeatureEnabled(flag: string, _userId?: string): boolean {
return flags.get(flag) ?? false;
},
getAllFlags(): Record<string, boolean> {
return Object.fromEntries(flags);
},
setFlag(flag: string, value: boolean): void {
flags.set(flag, value);
},
};
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src"]
}

View File

@ -0,0 +1,27 @@
{
"name": "@bytelyst/backend-telemetry",
"version": "0.1.0",
"description": "In-memory telemetry event buffer for Fastify product backends",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^5.7.3",
"vitest": "^3.0.5"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { createTelemetryBuffer } from './index.js';
describe('createTelemetryBuffer', () => {
it('buffers events when enabled', () => {
const buf = createTelemetryBuffer({ enabled: true });
buf.trackEvent('test.event', 'user-1', { key: 'val' });
const events = buf.getBufferedEvents();
expect(events).toHaveLength(1);
expect(events[0].event).toBe('test.event');
expect(events[0].userId).toBe('user-1');
expect(events[0].properties).toEqual({ key: 'val' });
expect(events[0].timestamp).toBeDefined();
});
it('is a no-op when disabled', () => {
const buf = createTelemetryBuffer({ enabled: false });
buf.trackEvent('test.event', 'user-1');
expect(buf.getBufferedEvents()).toHaveLength(0);
});
it('flushEvents returns and clears buffer', () => {
const buf = createTelemetryBuffer({ enabled: true });
buf.trackEvent('a');
buf.trackEvent('b');
const flushed = buf.flushEvents();
expect(flushed).toHaveLength(2);
expect(buf.getBufferedEvents()).toHaveLength(0);
});
it('getBufferedEvents returns a copy', () => {
const buf = createTelemetryBuffer({ enabled: true });
buf.trackEvent('a');
const copy = buf.getBufferedEvents();
copy.push({ event: 'fake' });
expect(buf.getBufferedEvents()).toHaveLength(1);
});
it('handles missing optional fields', () => {
const buf = createTelemetryBuffer({ enabled: true });
buf.trackEvent('minimal');
const events = buf.getBufferedEvents();
expect(events[0].userId).toBeUndefined();
expect(events[0].properties).toBeUndefined();
});
});

View File

@ -0,0 +1,50 @@
/**
* In-memory telemetry event buffer for product backends.
*
* Products call createTelemetryBuffer() with an enabled flag,
* then use trackEvent/getBufferedEvents/flushEvents as needed.
*/
export interface TelemetryEvent {
event: string;
userId?: string;
properties?: Record<string, unknown>;
timestamp?: string;
}
export interface TelemetryBuffer {
trackEvent(event: string, userId?: string, properties?: Record<string, unknown>): void;
getBufferedEvents(): TelemetryEvent[];
flushEvents(): TelemetryEvent[];
}
export interface TelemetryBufferOptions {
/** Master switch — when false, trackEvent is a no-op. */
enabled: boolean;
}
export function createTelemetryBuffer(opts: TelemetryBufferOptions): TelemetryBuffer {
const buffer: TelemetryEvent[] = [];
return {
trackEvent(event: string, userId?: string, properties?: Record<string, unknown>): void {
if (!opts.enabled) return;
buffer.push({
event,
userId,
properties,
timestamp: new Date().toISOString(),
});
},
getBufferedEvents(): TelemetryEvent[] {
return [...buffer];
},
flushEvents(): TelemetryEvent[] {
const flushed = [...buffer];
buffer.length = 0;
return flushed;
},
};
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src"]
}

18
pnpm-lock.yaml generated
View File

@ -363,6 +363,24 @@ importers:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.11)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages/backend-flags:
devDependencies:
typescript:
specifier: ^5.7.3
version: 5.9.3
vitest:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.11)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages/backend-telemetry:
devDependencies:
typescript:
specifier: ^5.7.3
version: 5.9.3
vitest:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.11)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages/blob:
dependencies:
'@bytelyst/storage':