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:
parent
08661bbd97
commit
0d8c0a5ffe
27
packages/backend-flags/package.json
Normal file
27
packages/backend-flags/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
43
packages/backend-flags/src/index.test.ts
Normal file
43
packages/backend-flags/src/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
38
packages/backend-flags/src/index.ts
Normal file
38
packages/backend-flags/src/index.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
9
packages/backend-flags/tsconfig.json
Normal file
9
packages/backend-flags/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
27
packages/backend-telemetry/package.json
Normal file
27
packages/backend-telemetry/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
46
packages/backend-telemetry/src/index.test.ts
Normal file
46
packages/backend-telemetry/src/index.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
50
packages/backend-telemetry/src/index.ts
Normal file
50
packages/backend-telemetry/src/index.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
9
packages/backend-telemetry/tsconfig.json
Normal file
9
packages/backend-telemetry/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@ -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':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user