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
|
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)
|
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:
|
packages/blob:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bytelyst/storage':
|
'@bytelyst/storage':
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user