mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: unique connection counting (#9074)
This commit is contained in:
		
							parent
							
								
									af1b6c8c37
								
							
						
					
					
						commit
						e559718581
					
				@ -134,6 +134,7 @@
 | 
				
			|||||||
    "hash-sum": "^2.0.0",
 | 
					    "hash-sum": "^2.0.0",
 | 
				
			||||||
    "helmet": "^6.0.0",
 | 
					    "helmet": "^6.0.0",
 | 
				
			||||||
    "http-errors": "^2.0.0",
 | 
					    "http-errors": "^2.0.0",
 | 
				
			||||||
 | 
					    "hyperloglog-lite": "^1.0.2",
 | 
				
			||||||
    "ip-address": "^10.0.1",
 | 
					    "ip-address": "^10.0.1",
 | 
				
			||||||
    "joi": "^17.13.3",
 | 
					    "joi": "^17.13.3",
 | 
				
			||||||
    "js-sha256": "^0.11.0",
 | 
					    "js-sha256": "^0.11.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -56,6 +56,7 @@ import { OnboardingStore } from '../features/onboarding/onboarding-store';
 | 
				
			|||||||
import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel';
 | 
					import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel';
 | 
				
			||||||
import { UserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store';
 | 
					import { UserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store';
 | 
				
			||||||
import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model';
 | 
					import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model';
 | 
				
			||||||
 | 
					import { UniqueConnectionStore } from '../features/unique-connection/unique-connection-store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createStores = (
 | 
					export const createStores = (
 | 
				
			||||||
    config: IUnleashConfig,
 | 
					    config: IUnleashConfig,
 | 
				
			||||||
@ -185,6 +186,7 @@ export const createStores = (
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        userUnsubscribeStore: new UserUnsubscribeStore(db),
 | 
					        userUnsubscribeStore: new UserUnsubscribeStore(db),
 | 
				
			||||||
        userSubscriptionsReadModel: new UserSubscriptionsReadModel(db),
 | 
					        userSubscriptionsReadModel: new UserSubscriptionsReadModel(db),
 | 
				
			||||||
 | 
					        uniqueConnectionStore: new UniqueConnectionStore(db),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -32,6 +32,7 @@ export const scheduleServices = async (
 | 
				
			|||||||
        frontendApiService,
 | 
					        frontendApiService,
 | 
				
			||||||
        clientMetricsServiceV2,
 | 
					        clientMetricsServiceV2,
 | 
				
			||||||
        integrationEventsService,
 | 
					        integrationEventsService,
 | 
				
			||||||
 | 
					        uniqueConnectionService,
 | 
				
			||||||
    } = services;
 | 
					    } = services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    schedulerService.schedule(
 | 
					    schedulerService.schedule(
 | 
				
			||||||
@ -179,4 +180,10 @@ export const scheduleServices = async (
 | 
				
			|||||||
        minutesToMilliseconds(15),
 | 
					        minutesToMilliseconds(15),
 | 
				
			||||||
        'cleanUpIntegrationEvents',
 | 
					        'cleanUpIntegrationEvents',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    schedulerService.schedule(
 | 
				
			||||||
 | 
					        uniqueConnectionService.sync.bind(uniqueConnectionService),
 | 
				
			||||||
 | 
					        minutesToMilliseconds(10),
 | 
				
			||||||
 | 
					        'uniqueConnectionService',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import type { IUniqueConnectionStore } from '../../types';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    TimedUniqueConnections,
 | 
				
			||||||
 | 
					    UniqueConnections,
 | 
				
			||||||
 | 
					} from './unique-connection-store-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class FakeUniqueConnectionStore implements IUniqueConnectionStore {
 | 
				
			||||||
 | 
					    private uniqueConnectionsRecord: Record<string, TimedUniqueConnections> =
 | 
				
			||||||
 | 
					        {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async insert(uniqueConnections: UniqueConnections): Promise<void> {
 | 
				
			||||||
 | 
					        this.uniqueConnectionsRecord[uniqueConnections.id] = {
 | 
				
			||||||
 | 
					            ...uniqueConnections,
 | 
				
			||||||
 | 
					            updatedAt: new Date(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async get(
 | 
				
			||||||
 | 
					        id: 'current' | 'previous',
 | 
				
			||||||
 | 
					    ): Promise<(UniqueConnections & { updatedAt: Date }) | null> {
 | 
				
			||||||
 | 
					        return this.uniqueConnectionsRecord[id] || null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteAll(): Promise<void> {
 | 
				
			||||||
 | 
					        this.uniqueConnectionsRecord = {};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,169 @@
 | 
				
			|||||||
 | 
					import { UniqueConnectionService } from './unique-connection-service';
 | 
				
			||||||
 | 
					import { FakeUniqueConnectionStore } from './fake-unique-connection-store';
 | 
				
			||||||
 | 
					import getLogger from '../../../test/fixtures/no-logger';
 | 
				
			||||||
 | 
					import type { IFlagResolver } from '../../types';
 | 
				
			||||||
 | 
					import { SDK_CONNECTION_ID_RECEIVED } from '../../metric-events';
 | 
				
			||||||
 | 
					import { addHours } from 'date-fns';
 | 
				
			||||||
 | 
					import EventEmitter from 'events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const alwaysOnFlagResolver = {
 | 
				
			||||||
 | 
					    isEnabled() {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as unknown as IFlagResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('sync first current bucket', async () => {
 | 
				
			||||||
 | 
					    const eventBus = new EventEmitter();
 | 
				
			||||||
 | 
					    const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus };
 | 
				
			||||||
 | 
					    const uniqueConnectionStore = new FakeUniqueConnectionStore();
 | 
				
			||||||
 | 
					    const uniqueConnectionService = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    uniqueConnectionService.listen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection1');
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection1');
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2');
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2');
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats = await uniqueConnectionService.getStats();
 | 
				
			||||||
 | 
					    expect(stats).toEqual({ previous: 0, current: 2 });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('sync first previous bucket', async () => {
 | 
				
			||||||
 | 
					    const eventBus = new EventEmitter();
 | 
				
			||||||
 | 
					    const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus };
 | 
				
			||||||
 | 
					    const uniqueConnectionStore = new FakeUniqueConnectionStore();
 | 
				
			||||||
 | 
					    const uniqueConnectionService = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    uniqueConnectionService.listen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection1');
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection3');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync(addHours(new Date(), 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats = await uniqueConnectionService.getStats();
 | 
				
			||||||
 | 
					    expect(stats).toEqual({ previous: 3, current: 0 });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('sync to existing current bucket from the same service', async () => {
 | 
				
			||||||
 | 
					    const eventBus = new EventEmitter();
 | 
				
			||||||
 | 
					    const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus };
 | 
				
			||||||
 | 
					    const uniqueConnectionStore = new FakeUniqueConnectionStore();
 | 
				
			||||||
 | 
					    const uniqueConnectionService = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    uniqueConnectionService.listen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection2');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection3');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats = await uniqueConnectionService.getStats();
 | 
				
			||||||
 | 
					    expect(stats).toEqual({ previous: 0, current: 3 });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('sync to existing current bucket from another service', async () => {
 | 
				
			||||||
 | 
					    const eventBus = new EventEmitter();
 | 
				
			||||||
 | 
					    const config = {
 | 
				
			||||||
 | 
					        flagResolver: alwaysOnFlagResolver,
 | 
				
			||||||
 | 
					        getLogger,
 | 
				
			||||||
 | 
					        eventBus: eventBus,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const uniqueConnectionStore = new FakeUniqueConnectionStore();
 | 
				
			||||||
 | 
					    const uniqueConnectionService1 = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const uniqueConnectionService2 = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService1.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService1.count('connection2');
 | 
				
			||||||
 | 
					    await uniqueConnectionService1.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService2.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService2.count('connection3');
 | 
				
			||||||
 | 
					    await uniqueConnectionService2.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats1 = await uniqueConnectionService1.getStats();
 | 
				
			||||||
 | 
					    expect(stats1).toEqual({ previous: 0, current: 3 });
 | 
				
			||||||
 | 
					    const stats2 = await uniqueConnectionService2.getStats();
 | 
				
			||||||
 | 
					    expect(stats2).toEqual({ previous: 0, current: 3 });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('sync to existing previous bucket from another service', async () => {
 | 
				
			||||||
 | 
					    const eventBus = new EventEmitter();
 | 
				
			||||||
 | 
					    const config = {
 | 
				
			||||||
 | 
					        flagResolver: alwaysOnFlagResolver,
 | 
				
			||||||
 | 
					        getLogger,
 | 
				
			||||||
 | 
					        eventBus: eventBus,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const uniqueConnectionStore = new FakeUniqueConnectionStore();
 | 
				
			||||||
 | 
					    const uniqueConnectionService1 = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const uniqueConnectionService2 = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService1.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService1.count('connection2');
 | 
				
			||||||
 | 
					    await uniqueConnectionService1.sync(addHours(new Date(), 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService2.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService2.count('connection3');
 | 
				
			||||||
 | 
					    await uniqueConnectionService2.sync(addHours(new Date(), 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats1 = await uniqueConnectionService1.getStats();
 | 
				
			||||||
 | 
					    expect(stats1).toEqual({ previous: 3, current: 0 });
 | 
				
			||||||
 | 
					    const stats2 = await uniqueConnectionService2.getStats();
 | 
				
			||||||
 | 
					    expect(stats2).toEqual({ previous: 3, current: 0 });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('populate previous and current', async () => {
 | 
				
			||||||
 | 
					    const eventBus = new EventEmitter();
 | 
				
			||||||
 | 
					    const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus };
 | 
				
			||||||
 | 
					    const uniqueConnectionStore = new FakeUniqueConnectionStore();
 | 
				
			||||||
 | 
					    const uniqueConnectionService = new UniqueConnectionService(
 | 
				
			||||||
 | 
					        { uniqueConnectionStore },
 | 
				
			||||||
 | 
					        config,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection1');
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection2');
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync();
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection3');
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync(addHours(new Date(), 1));
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync(addHours(new Date(), 1)); // deliberate duplicate call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection3');
 | 
				
			||||||
 | 
					    uniqueConnectionService.count('connection4');
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync(addHours(new Date(), 1));
 | 
				
			||||||
 | 
					    await uniqueConnectionService.sync(addHours(new Date(), 1)); // deliberate duplicate call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats = await uniqueConnectionService.getStats();
 | 
				
			||||||
 | 
					    expect(stats).toEqual({ previous: 3, current: 2 });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					import type { IUnleashConfig } from '../../types/option';
 | 
				
			||||||
 | 
					import type { IFlagResolver, IUnleashStores } from '../../types';
 | 
				
			||||||
 | 
					import type { Logger } from '../../logger';
 | 
				
			||||||
 | 
					import type { IUniqueConnectionStore } from './unique-connection-store-type';
 | 
				
			||||||
 | 
					import HyperLogLog from 'hyperloglog-lite';
 | 
				
			||||||
 | 
					import type EventEmitter from 'events';
 | 
				
			||||||
 | 
					import { SDK_CONNECTION_ID_RECEIVED } from '../../metric-events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HyperLogLog will create 2^n registers
 | 
				
			||||||
 | 
					const n = 12;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UniqueConnectionService {
 | 
				
			||||||
 | 
					    private logger: Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private uniqueConnectionStore: IUniqueConnectionStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private flagResolver: IFlagResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private eventBus: EventEmitter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private activeHour: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private hll = HyperLogLog(n);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            uniqueConnectionStore,
 | 
				
			||||||
 | 
					        }: Pick<IUnleashStores, 'uniqueConnectionStore'>,
 | 
				
			||||||
 | 
					        config: Pick<IUnleashConfig, 'getLogger' | 'flagResolver' | 'eventBus'>,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        this.uniqueConnectionStore = uniqueConnectionStore;
 | 
				
			||||||
 | 
					        this.logger = config.getLogger('services/unique-connection-service.ts');
 | 
				
			||||||
 | 
					        this.flagResolver = config.flagResolver;
 | 
				
			||||||
 | 
					        this.eventBus = config.eventBus;
 | 
				
			||||||
 | 
					        this.activeHour = new Date().getHours();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    listen() {
 | 
				
			||||||
 | 
					        this.eventBus.on(SDK_CONNECTION_ID_RECEIVED, this.count.bind(this));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    count(connectionId: string) {
 | 
				
			||||||
 | 
					        if (!this.flagResolver.isEnabled('uniqueSdkTracking')) return;
 | 
				
			||||||
 | 
					        this.hll.add(HyperLogLog.hash(connectionId));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getStats() {
 | 
				
			||||||
 | 
					        const [previous, current] = await Promise.all([
 | 
				
			||||||
 | 
					            this.uniqueConnectionStore.get('previous'),
 | 
				
			||||||
 | 
					            this.uniqueConnectionStore.get('current'),
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        const previousHll = HyperLogLog(n);
 | 
				
			||||||
 | 
					        if (previous) {
 | 
				
			||||||
 | 
					            previousHll.merge({ n, buckets: previous.hll });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const currentHll = HyperLogLog(n);
 | 
				
			||||||
 | 
					        if (current) {
 | 
				
			||||||
 | 
					            currentHll.merge({ n, buckets: current.hll });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return { previous: previousHll.count(), current: currentHll.count() };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async sync(currentTime = new Date()): Promise<void> {
 | 
				
			||||||
 | 
					        if (!this.flagResolver.isEnabled('uniqueSdkTracking')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const currentHour = currentTime.getHours();
 | 
				
			||||||
 | 
					        const currentBucket = await this.uniqueConnectionStore.get('current');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.activeHour !== currentHour && currentBucket) {
 | 
				
			||||||
 | 
					            if (currentBucket.updatedAt.getHours() < currentHour) {
 | 
				
			||||||
 | 
					                this.hll.merge({ n, buckets: currentBucket.hll });
 | 
				
			||||||
 | 
					                await this.uniqueConnectionStore.insert({
 | 
				
			||||||
 | 
					                    hll: this.hll.output().buckets,
 | 
				
			||||||
 | 
					                    id: 'previous',
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const previousBucket =
 | 
				
			||||||
 | 
					                    await this.uniqueConnectionStore.get('previous');
 | 
				
			||||||
 | 
					                if (previousBucket) {
 | 
				
			||||||
 | 
					                    this.hll.merge({ n, buckets: previousBucket.hll });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                await this.uniqueConnectionStore.insert({
 | 
				
			||||||
 | 
					                    hll: this.hll.output().buckets,
 | 
				
			||||||
 | 
					                    id: 'previous',
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.activeHour = currentHour;
 | 
				
			||||||
 | 
					            this.hll = HyperLogLog(n);
 | 
				
			||||||
 | 
					        } else if (currentBucket) {
 | 
				
			||||||
 | 
					            this.hll.merge({ n, buckets: currentBucket.hll });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.uniqueConnectionStore.insert({
 | 
				
			||||||
 | 
					            hll: this.hll.output().buckets,
 | 
				
			||||||
 | 
					            id: 'current',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					export type UniqueConnections = {
 | 
				
			||||||
 | 
					    hll: Buffer;
 | 
				
			||||||
 | 
					    id: 'current' | 'previous';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TimedUniqueConnections = UniqueConnections & {
 | 
				
			||||||
 | 
					    updatedAt: Date;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IUniqueConnectionStore {
 | 
				
			||||||
 | 
					    insert(uniqueConnections: UniqueConnections): Promise<void>;
 | 
				
			||||||
 | 
					    get(id: 'current' | 'previous'): Promise<TimedUniqueConnections | null>;
 | 
				
			||||||
 | 
					    deleteAll(): Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
 | 
				
			||||||
 | 
					import getLogger from '../../../test/fixtures/no-logger';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    IUniqueConnectionStore,
 | 
				
			||||||
 | 
					    IUnleashStores,
 | 
				
			||||||
 | 
					} from '../../../lib/types';
 | 
				
			||||||
 | 
					import HyperLogLog from 'hyperloglog-lite';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let stores: IUnleashStores;
 | 
				
			||||||
 | 
					let db: ITestDb;
 | 
				
			||||||
 | 
					let uniqueConnectionStore: IUniqueConnectionStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					beforeAll(async () => {
 | 
				
			||||||
 | 
					    db = await dbInit('unique_connections_store', getLogger);
 | 
				
			||||||
 | 
					    stores = db.stores;
 | 
				
			||||||
 | 
					    uniqueConnectionStore = stores.uniqueConnectionStore;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					afterAll(async () => {
 | 
				
			||||||
 | 
					    await db.destroy();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					beforeEach(async () => {
 | 
				
			||||||
 | 
					    await uniqueConnectionStore.deleteAll();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should store empty HyperLogLog buffer', async () => {
 | 
				
			||||||
 | 
					    const hll = HyperLogLog(12);
 | 
				
			||||||
 | 
					    await uniqueConnectionStore.insert({
 | 
				
			||||||
 | 
					        id: 'current',
 | 
				
			||||||
 | 
					        hll: hll.output().buckets,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fetchedHll = await uniqueConnectionStore.get('current');
 | 
				
			||||||
 | 
					    hll.merge({ n: 12, buckets: fetchedHll!.hll });
 | 
				
			||||||
 | 
					    expect(hll.count()).toBe(0);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should store non empty HyperLogLog buffer', async () => {
 | 
				
			||||||
 | 
					    const hll = HyperLogLog(12);
 | 
				
			||||||
 | 
					    hll.add(HyperLogLog.hash('connection-1'));
 | 
				
			||||||
 | 
					    hll.add(HyperLogLog.hash('connection-2'));
 | 
				
			||||||
 | 
					    await uniqueConnectionStore.insert({
 | 
				
			||||||
 | 
					        id: 'current',
 | 
				
			||||||
 | 
					        hll: hll.output().buckets,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fetchedHll = await uniqueConnectionStore.get('current');
 | 
				
			||||||
 | 
					    const emptyHll = HyperLogLog(12);
 | 
				
			||||||
 | 
					    emptyHll.merge({ n: 12, buckets: fetchedHll!.hll });
 | 
				
			||||||
 | 
					    expect(hll.count()).toBe(2);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should indicate when no entry', async () => {
 | 
				
			||||||
 | 
					    const fetchedHll = await uniqueConnectionStore.get('current');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(fetchedHll).toBeNull();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					import type { Db } from '../../db/db';
 | 
				
			||||||
 | 
					import type { IUniqueConnectionStore } from '../../types';
 | 
				
			||||||
 | 
					import type { UniqueConnections } from './unique-connection-store-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UniqueConnectionStore implements IUniqueConnectionStore {
 | 
				
			||||||
 | 
					    private db: Db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(db: Db) {
 | 
				
			||||||
 | 
					        this.db = db;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async insert(uniqueConnections: UniqueConnections): Promise<void> {
 | 
				
			||||||
 | 
					        await this.db<UniqueConnections>('unique_connections')
 | 
				
			||||||
 | 
					            .insert({ id: uniqueConnections.id, hll: uniqueConnections.hll })
 | 
				
			||||||
 | 
					            .onConflict('id')
 | 
				
			||||||
 | 
					            .merge();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async get(
 | 
				
			||||||
 | 
					        id: 'current' | 'previous',
 | 
				
			||||||
 | 
					    ): Promise<(UniqueConnections & { updatedAt: Date }) | null> {
 | 
				
			||||||
 | 
					        const row = await this.db('unique_connections')
 | 
				
			||||||
 | 
					            .select('id', 'hll', 'updated_at')
 | 
				
			||||||
 | 
					            .where('id', id)
 | 
				
			||||||
 | 
					            .first();
 | 
				
			||||||
 | 
					        return row
 | 
				
			||||||
 | 
					            ? { id: row.id, hll: row.hll, updatedAt: row.updated_at }
 | 
				
			||||||
 | 
					            : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteAll(): Promise<void> {
 | 
				
			||||||
 | 
					        await this.db('unique_connections').delete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import type EventEmitter from 'events';
 | 
					import type EventEmitter from 'events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const REQUEST_TIME = 'request_time';
 | 
					const REQUEST_TIME = 'request_time';
 | 
				
			||||||
 | 
					const SDK_CONNECTION_ID_RECEIVED = 'sdk_connection_id_received';
 | 
				
			||||||
const DB_TIME = 'db_time';
 | 
					const DB_TIME = 'db_time';
 | 
				
			||||||
const FUNCTION_TIME = 'function_time';
 | 
					const FUNCTION_TIME = 'function_time';
 | 
				
			||||||
const SCHEDULER_JOB_TIME = 'scheduler_job_time';
 | 
					const SCHEDULER_JOB_TIME = 'scheduler_job_time';
 | 
				
			||||||
@ -21,6 +22,7 @@ const CLIENT_DELTA_MEMORY = 'client_delta_memory';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type MetricEvent =
 | 
					type MetricEvent =
 | 
				
			||||||
    | typeof REQUEST_TIME
 | 
					    | typeof REQUEST_TIME
 | 
				
			||||||
 | 
					    | typeof SDK_CONNECTION_ID_RECEIVED
 | 
				
			||||||
    | typeof DB_TIME
 | 
					    | typeof DB_TIME
 | 
				
			||||||
    | typeof FUNCTION_TIME
 | 
					    | typeof FUNCTION_TIME
 | 
				
			||||||
    | typeof SCHEDULER_JOB_TIME
 | 
					    | typeof SCHEDULER_JOB_TIME
 | 
				
			||||||
@ -71,6 +73,7 @@ const onMetricEvent = <T extends MetricEvent>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
    REQUEST_TIME,
 | 
					    REQUEST_TIME,
 | 
				
			||||||
 | 
					    SDK_CONNECTION_ID_RECEIVED,
 | 
				
			||||||
    DB_TIME,
 | 
					    DB_TIME,
 | 
				
			||||||
    SCHEDULER_JOB_TIME,
 | 
					    SCHEDULER_JOB_TIME,
 | 
				
			||||||
    FUNCTION_TIME,
 | 
					    FUNCTION_TIME,
 | 
				
			||||||
 | 
				
			|||||||
@ -66,6 +66,7 @@ describe('responseTimeMetrics new behavior', () => {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
            path: 'should-not-be-used',
 | 
					            path: 'should-not-be-used',
 | 
				
			||||||
 | 
					            headers: {},
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // @ts-expect-error req and res doesn't have all properties
 | 
					        // @ts-expect-error req and res doesn't have all properties
 | 
				
			||||||
@ -98,6 +99,7 @@ describe('responseTimeMetrics new behavior', () => {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
        const reqWithoutRoute = {
 | 
					        const reqWithoutRoute = {
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
 | 
					            headers: {},
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // @ts-expect-error req and res doesn't have all properties
 | 
					        // @ts-expect-error req and res doesn't have all properties
 | 
				
			||||||
@ -132,6 +134,7 @@ describe('responseTimeMetrics new behavior', () => {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
        const reqWithoutRoute = {
 | 
					        const reqWithoutRoute = {
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
 | 
					            headers: {},
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // @ts-expect-error req and res doesn't have all properties
 | 
					        // @ts-expect-error req and res doesn't have all properties
 | 
				
			||||||
@ -166,6 +169,7 @@ describe('responseTimeMetrics new behavior', () => {
 | 
				
			|||||||
            const reqWithoutRoute = {
 | 
					            const reqWithoutRoute = {
 | 
				
			||||||
                method: 'GET',
 | 
					                method: 'GET',
 | 
				
			||||||
                path,
 | 
					                path,
 | 
				
			||||||
 | 
					                headers: {},
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // @ts-expect-error req and res doesn't have all properties
 | 
					            // @ts-expect-error req and res doesn't have all properties
 | 
				
			||||||
@ -210,6 +214,7 @@ describe('responseTimeMetrics new behavior', () => {
 | 
				
			|||||||
            const reqWithoutRoute = {
 | 
					            const reqWithoutRoute = {
 | 
				
			||||||
                method: 'GET',
 | 
					                method: 'GET',
 | 
				
			||||||
                path,
 | 
					                path,
 | 
				
			||||||
 | 
					                headers: {},
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // @ts-expect-error req and res doesn't have all properties
 | 
					            // @ts-expect-error req and res doesn't have all properties
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import * as responseTime from 'response-time';
 | 
					import * as responseTime from 'response-time';
 | 
				
			||||||
import type EventEmitter from 'events';
 | 
					import type EventEmitter from 'events';
 | 
				
			||||||
import { REQUEST_TIME } from '../metric-events';
 | 
					import { REQUEST_TIME, SDK_CONNECTION_ID_RECEIVED } from '../metric-events';
 | 
				
			||||||
import type { IFlagResolver } from '../types/experimental';
 | 
					import type { IFlagResolver } from '../types/experimental';
 | 
				
			||||||
import type { InstanceStatsService } from '../services';
 | 
					import type { InstanceStatsService } from '../services';
 | 
				
			||||||
import type { RequestHandler } from 'express';
 | 
					import type { RequestHandler } from 'express';
 | 
				
			||||||
@ -66,6 +66,11 @@ export function responseTimeMetrics(
 | 
				
			|||||||
                req.query.appName;
 | 
					                req.query.appName;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const connectionId = req.headers['x-unleash-connection-id'];
 | 
				
			||||||
 | 
					        if (connectionId && flagResolver.isEnabled('uniqueSdkTracking')) {
 | 
				
			||||||
 | 
					            eventBus.emit(SDK_CONNECTION_ID_RECEIVED, connectionId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const timingInfo = {
 | 
					        const timingInfo = {
 | 
				
			||||||
            path: pathname,
 | 
					            path: pathname,
 | 
				
			||||||
            method: req.method,
 | 
					            method: req.method,
 | 
				
			||||||
 | 
				
			|||||||
@ -157,6 +157,7 @@ import {
 | 
				
			|||||||
    createContextService,
 | 
					    createContextService,
 | 
				
			||||||
    createFakeContextService,
 | 
					    createFakeContextService,
 | 
				
			||||||
} from '../features/context/createContextService';
 | 
					} from '../features/context/createContextService';
 | 
				
			||||||
 | 
					import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createServices = (
 | 
					export const createServices = (
 | 
				
			||||||
    stores: IUnleashStores,
 | 
					    stores: IUnleashStores,
 | 
				
			||||||
@ -403,6 +404,9 @@ export const createServices = (
 | 
				
			|||||||
    const featureLifecycleService = transactionalFeatureLifecycleService;
 | 
					    const featureLifecycleService = transactionalFeatureLifecycleService;
 | 
				
			||||||
    featureLifecycleService.listen();
 | 
					    featureLifecycleService.listen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const uniqueConnectionService = new UniqueConnectionService(stores, config);
 | 
				
			||||||
 | 
					    uniqueConnectionService.listen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onboardingService = db
 | 
					    const onboardingService = db
 | 
				
			||||||
        ? createOnboardingService(config)(db)
 | 
					        ? createOnboardingService(config)(db)
 | 
				
			||||||
        : createFakeOnboardingService(config).onboardingService;
 | 
					        : createFakeOnboardingService(config).onboardingService;
 | 
				
			||||||
@ -484,6 +488,7 @@ export const createServices = (
 | 
				
			|||||||
        personalDashboardService,
 | 
					        personalDashboardService,
 | 
				
			||||||
        projectStatusService,
 | 
					        projectStatusService,
 | 
				
			||||||
        transactionalUserSubscriptionsService,
 | 
					        transactionalUserSubscriptionsService,
 | 
				
			||||||
 | 
					        uniqueConnectionService,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -537,4 +542,5 @@ export {
 | 
				
			|||||||
    PersonalDashboardService,
 | 
					    PersonalDashboardService,
 | 
				
			||||||
    ProjectStatusService,
 | 
					    ProjectStatusService,
 | 
				
			||||||
    UserSubscriptionsService,
 | 
					    UserSubscriptionsService,
 | 
				
			||||||
 | 
					    UniqueConnectionService,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -59,6 +59,7 @@ import type { OnboardingService } from '../features/onboarding/onboarding-servic
 | 
				
			|||||||
import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service';
 | 
					import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service';
 | 
				
			||||||
import type { ProjectStatusService } from '../features/project-status/project-status-service';
 | 
					import type { ProjectStatusService } from '../features/project-status/project-status-service';
 | 
				
			||||||
import type { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service';
 | 
					import type { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service';
 | 
				
			||||||
 | 
					import type { UniqueConnectionService } from '../features/unique-connection/unique-connection-service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IUnleashServices {
 | 
					export interface IUnleashServices {
 | 
				
			||||||
    transactionalAccessService: WithTransactional<AccessService>;
 | 
					    transactionalAccessService: WithTransactional<AccessService>;
 | 
				
			||||||
@ -131,4 +132,5 @@ export interface IUnleashServices {
 | 
				
			|||||||
    personalDashboardService: PersonalDashboardService;
 | 
					    personalDashboardService: PersonalDashboardService;
 | 
				
			||||||
    projectStatusService: ProjectStatusService;
 | 
					    projectStatusService: ProjectStatusService;
 | 
				
			||||||
    transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
 | 
					    transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
 | 
				
			||||||
 | 
					    uniqueConnectionService: UniqueConnectionService;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -53,6 +53,7 @@ import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-mod
 | 
				
			|||||||
import { IOnboardingStore } from '../features/onboarding/onboarding-store-type';
 | 
					import { IOnboardingStore } from '../features/onboarding/onboarding-store-type';
 | 
				
			||||||
import type { IUserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store-type';
 | 
					import type { IUserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store-type';
 | 
				
			||||||
import type { IUserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model-type';
 | 
					import type { IUserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model-type';
 | 
				
			||||||
 | 
					import { IUniqueConnectionStore } from '../features/unique-connection/unique-connection-store-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IUnleashStores {
 | 
					export interface IUnleashStores {
 | 
				
			||||||
    accessStore: IAccessStore;
 | 
					    accessStore: IAccessStore;
 | 
				
			||||||
@ -110,6 +111,7 @@ export interface IUnleashStores {
 | 
				
			|||||||
    onboardingStore: IOnboardingStore;
 | 
					    onboardingStore: IOnboardingStore;
 | 
				
			||||||
    userUnsubscribeStore: IUserUnsubscribeStore;
 | 
					    userUnsubscribeStore: IUserUnsubscribeStore;
 | 
				
			||||||
    userSubscriptionsReadModel: IUserSubscriptionsReadModel;
 | 
					    userSubscriptionsReadModel: IUserSubscriptionsReadModel;
 | 
				
			||||||
 | 
					    uniqueConnectionStore: IUniqueConnectionStore;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
@ -165,4 +167,5 @@ export {
 | 
				
			|||||||
    type IProjectReadModel,
 | 
					    type IProjectReadModel,
 | 
				
			||||||
    IOnboardingStore,
 | 
					    IOnboardingStore,
 | 
				
			||||||
    type IUserSubscriptionsReadModel,
 | 
					    type IUserSubscriptionsReadModel,
 | 
				
			||||||
 | 
					    IUniqueConnectionStore,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -55,6 +55,7 @@ process.nextTick(async () => {
 | 
				
			|||||||
                        flagOverviewRedesign: false,
 | 
					                        flagOverviewRedesign: false,
 | 
				
			||||||
                        granularAdminPermissions: true,
 | 
					                        granularAdminPermissions: true,
 | 
				
			||||||
                        deltaApi: true,
 | 
					                        deltaApi: true,
 | 
				
			||||||
 | 
					                        uniqueSdkTracking: true,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                authentication: {
 | 
					                authentication: {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								src/test/fixtures/store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/test/fixtures/store.ts
									
									
									
									
										vendored
									
									
								
							@ -56,6 +56,7 @@ import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboardi
 | 
				
			|||||||
import { createFakeOnboardingReadModel } from '../../lib/features/onboarding/createOnboardingReadModel';
 | 
					import { createFakeOnboardingReadModel } from '../../lib/features/onboarding/createOnboardingReadModel';
 | 
				
			||||||
import { FakeUserUnsubscribeStore } from '../../lib/features/user-subscriptions/fake-user-unsubscribe-store';
 | 
					import { FakeUserUnsubscribeStore } from '../../lib/features/user-subscriptions/fake-user-unsubscribe-store';
 | 
				
			||||||
import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model';
 | 
					import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model';
 | 
				
			||||||
 | 
					import { FakeUniqueConnectionStore } from '../../lib/features/unique-connection/fake-unique-connection-store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const db = {
 | 
					const db = {
 | 
				
			||||||
    select: () => ({
 | 
					    select: () => ({
 | 
				
			||||||
@ -121,6 +122,7 @@ const createStores: () => IUnleashStores = () => {
 | 
				
			|||||||
        onboardingStore: new FakeOnboardingStore(),
 | 
					        onboardingStore: new FakeOnboardingStore(),
 | 
				
			||||||
        userUnsubscribeStore: new FakeUserUnsubscribeStore(),
 | 
					        userUnsubscribeStore: new FakeUserUnsubscribeStore(),
 | 
				
			||||||
        userSubscriptionsReadModel: new FakeUserSubscriptionsReadModel(),
 | 
					        userSubscriptionsReadModel: new FakeUserSubscriptionsReadModel(),
 | 
				
			||||||
 | 
					        uniqueConnectionStore: new FakeUniqueConnectionStore(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								yarn.lock
									
									
									
									
									
								
							@ -4919,6 +4919,15 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"hyperloglog-lite@npm:^1.0.2":
 | 
				
			||||||
 | 
					  version: 1.0.2
 | 
				
			||||||
 | 
					  resolution: "hyperloglog-lite@npm:1.0.2"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    murmurhash32-node: "npm:^1.0.1"
 | 
				
			||||||
 | 
					  checksum: 10c0/3077b9dba1bac384b842a70d1b17da58449d3e633936ef7bd03a3386613e59c413f5f886d9383d14c3fe31eac524abe28a99025d43c446e57aa4175b17675450
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"iconv-lite@npm:0.4.24":
 | 
					"iconv-lite@npm:0.4.24":
 | 
				
			||||||
  version: 0.4.24
 | 
					  version: 0.4.24
 | 
				
			||||||
  resolution: "iconv-lite@npm:0.4.24"
 | 
					  resolution: "iconv-lite@npm:0.4.24"
 | 
				
			||||||
@ -6831,6 +6840,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"murmurhash32-node@npm:^1.0.1":
 | 
				
			||||||
 | 
					  version: 1.0.1
 | 
				
			||||||
 | 
					  resolution: "murmurhash32-node@npm:1.0.1"
 | 
				
			||||||
 | 
					  checksum: 10c0/06a36a2f0d0c6855ce131c2a5c225c3096f53bf36898eb2683b2200f782577cde07f07485792d6e85798ea74f4dd95e836058fbab07c49cfcbc0a79b168ab654
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"murmurhash3js@npm:^3.0.1":
 | 
					"murmurhash3js@npm:^3.0.1":
 | 
				
			||||||
  version: 3.0.1
 | 
					  version: 3.0.1
 | 
				
			||||||
  resolution: "murmurhash3js@npm:3.0.1"
 | 
					  resolution: "murmurhash3js@npm:3.0.1"
 | 
				
			||||||
@ -9311,6 +9327,7 @@ __metadata:
 | 
				
			|||||||
    helmet: "npm:^6.0.0"
 | 
					    helmet: "npm:^6.0.0"
 | 
				
			||||||
    http-errors: "npm:^2.0.0"
 | 
					    http-errors: "npm:^2.0.0"
 | 
				
			||||||
    husky: "npm:^9.0.11"
 | 
					    husky: "npm:^9.0.11"
 | 
				
			||||||
 | 
					    hyperloglog-lite: "npm:^1.0.2"
 | 
				
			||||||
    ip-address: "npm:^10.0.1"
 | 
					    ip-address: "npm:^10.0.1"
 | 
				
			||||||
    jest: "npm:29.7.0"
 | 
					    jest: "npm:29.7.0"
 | 
				
			||||||
    jest-junit: "npm:^16.0.0"
 | 
					    jest-junit: "npm:^16.0.0"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user