UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

1,169 lines • 42.3 kB
import { EventEmitter } from 'events'; import { register } from 'prom-client'; import { ClientFeatureToggleDelta, filterEventsByQuery, } from './client-feature-toggle-delta.js'; import { DeltaCache } from './delta-cache.js'; import { FEATURE_PROJECT_CHANGE, SEGMENT_DELETED, } from '../../../events/index.js'; const createLogger = () => ({ error: () => undefined, info: () => undefined, warn: () => undefined, }); const createDeltaConfig = () => ({ eventBus: new EventEmitter(), getLogger: () => createLogger(), }); describe('filterEventsByQuery', () => { const mockEvents = [ { eventId: 1, type: 'feature-updated', feature: { name: 'test-feature', project: 'project1', enabled: true, }, }, { eventId: 2, type: 'feature-updated', feature: { name: 'alpha-feature', project: 'project2', enabled: true, }, }, { eventId: 3, type: 'feature-removed', featureName: 'beta-feature', project: 'project3', }, { eventId: 4, type: 'segment-updated', segment: { id: 1, name: 'my-segment', constraints: [] }, }, { eventId: 5, type: 'segment-removed', segmentId: 2 }, ]; test('filters events based on eventId', () => { const requiredRevisionId = 2; const result = filterEventsByQuery(mockEvents, requiredRevisionId, ['project3'], '', new Set([1, 2])); expect(result).toEqual([ { eventId: 3, type: 'feature-removed', featureName: 'beta-feature', project: 'project3', }, { eventId: 4, type: 'segment-updated', segment: { id: 1, name: 'my-segment', constraints: [] }, }, { eventId: 5, type: 'segment-removed', segmentId: 2 }, ]); }); test('returns all projects', () => { const result = filterEventsByQuery(mockEvents, 0, ['*'], '', new Set([1])); expect(result).toEqual(mockEvents); }); test('filters by name prefix', () => { const result = filterEventsByQuery(mockEvents, 0, ['project1', 'project2'], 'alpha', new Set([1, 2])); expect(result).toEqual([ { eventId: 2, type: 'feature-updated', feature: { name: 'alpha-feature', project: 'project2', enabled: true, }, }, { eventId: 4, type: 'segment-updated', segment: { id: 1, name: 'my-segment', constraints: [] }, }, { eventId: 5, type: 'segment-removed', segmentId: 2 }, ]); }); test('filters by project list', () => { const referencedSegmentIds = new Set([1, 2]); const result = filterEventsByQuery(mockEvents, 0, ['project3'], 'beta', referencedSegmentIds); expect(result).toEqual([ { eventId: 3, type: 'feature-removed', featureName: 'beta-feature', project: 'project3', }, { eventId: 4, type: 'segment-updated', segment: { id: 1, name: 'my-segment', constraints: [] }, }, // we propagate segment removed. Once dereferenced removing them should save memory { eventId: 5, type: 'segment-removed', segmentId: 2 }, ]); }); }); describe('DeltaCache hydration ordering', () => { test('keeps hydration features sorted after updates', () => { const cache = new DeltaCache({ eventId: 1, type: 'hydration', features: [ { name: 'bravo', enabled: true }, { name: 'charlie', enabled: true }, ], segments: [{ id: 2, name: 'segment-b', constraints: [] }], }); cache.addEvents([ { eventId: 2, type: 'feature-updated', feature: { name: 'alpha', enabled: true }, }, ]); const hydration = cache.getHydrationEvent(); expect(hydration.features.map((feature) => feature.name)).toEqual([ 'alpha', 'bravo', 'charlie', ]); }); test('keeps hydration segments sorted after updates', () => { const cache = new DeltaCache({ eventId: 1, type: 'hydration', features: [{ name: 'alpha', enabled: true }], segments: [ { id: 3, name: 'segment-c', constraints: [] }, { id: 4, name: 'segment-d', constraints: [] }, ], }); cache.addEvents([ { eventId: 2, type: 'segment-updated', segment: { id: 2, name: 'segment-b', constraints: [] }, }, { eventId: 3, type: 'segment-updated', segment: { id: 1, name: 'segment-b', constraints: [] }, }, ]); const hydration = cache.getHydrationEvent(); expect(hydration.segments.map((segment) => segment.id)).toEqual([ 1, 2, 3, 4, ]); }); }); describe('ClientFeatureToggleDelta bootstrap behavior', () => { test('segment-created alone does not advance visible revision for an environment where it is unused', async () => { let currentRevisionId = 1; const delta = new ClientFeatureToggleDelta({ getAll: async ({ environment, toggleNames = [], }) => { const developmentFeature = { name: 'first', project: 'default', enabled: false, }; if (environment !== 'development') { return []; } if (toggleNames.length === 0) { return [developmentFeature]; } return toggleNames.includes('first') ? [developmentFeature] : []; }, }, { getAllForClientIds: async (ids) => ids?.includes(101) ? [{ id: 101, name: 'segment-a', constraints: [] }] : [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: 'segment-created', data: { id: 101, name: 'segment-a' }, createdAt: new Date(), }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); const baseline = await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); currentRevisionId = 2; await delta.onUpdateRevisionEvent(); const result = await delta.getDelta(1, { environment: 'development', project: ['default'], }); expect(baseline?.events[0]?.eventId).toBe(1); expect(result).toBeUndefined(); }); test('segment-created followed by unrelated environment change does not advance visible revision and hydration stays in parity', async () => { const currentRevisionId = 1; const createReadModel = () => ({ getAll: async ({ environment, toggleNames = [], }) => { const featureByEnvironment = { development: { name: 'first', project: 'default', enabled: false, }, production: { name: 'first', project: 'default', enabled: true, }, }; const feature = featureByEnvironment[environment]; if (!feature) { return []; } if (toggleNames.length === 0) { return [feature]; } return toggleNames.includes('first') ? [feature] : []; }, }); const createSegmentModel = () => ({ getAllForClientIds: async (ids) => ids?.includes(101) ? [{ id: 101, name: 'segment-a', constraints: [] }] : [], }); const createEventStore = () => ({ getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: 'segment-created', data: { id: 101, name: 'segment-a' }, createdAt: new Date(), }, { id: 3, type: 'feature-updated', featureName: 'first', project: 'default', environment: 'production', }, ], getMaxRevisionId: async () => currentRevisionId, }); const liveDelta = new ClientFeatureToggleDelta(createReadModel(), createSegmentModel(), createEventStore(), { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); const initialHydration = await liveDelta.getDelta(undefined, { environment: 'development', project: ['default'], }); expect(initialHydration?.events[0]?.eventId).toBe(1); await liveDelta.onUpdateRevisionEvent(); const liveResult = await liveDelta.getDelta(1, { environment: 'development', project: ['default'], }); expect(liveResult).toBeUndefined(); const freshDelta = new ClientFeatureToggleDelta(createReadModel(), createSegmentModel(), createEventStore(), { on: () => undefined, }, {}, createDeltaConfig()); const freshHydration = await freshDelta.getDelta(undefined, { environment: 'development', project: ['default'], }); expect(freshHydration?.events[0]?.eventId).toBe(1); }); test('segment-updated only advances visible revision when the segment is referenced by a visible feature', async () => { let currentRevisionId = 1; const createEventStore = () => ({ getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 1, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: 'segment-updated', data: { id: 101, name: 'segment-a' }, createdAt: new Date(), }, ], getMaxRevisionId: async () => currentRevisionId, }); const createSegmentModel = () => ({ getAllForClientIds: async (ids) => ids?.includes(101) ? [{ id: 101, name: 'segment-a', constraints: [] }] : [], }); const unreferencedSegment = new ClientFeatureToggleDelta({ getAll: async () => [ { name: 'first', project: 'default', enabled: false, }, ], }, createSegmentModel(), createEventStore(), { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); await unreferencedSegment.getDelta(undefined, { environment: 'development', project: ['default'], }); currentRevisionId = 2; await unreferencedSegment.onUpdateRevisionEvent(); const unusedResult = await unreferencedSegment.getDelta(1, { environment: 'development', project: ['default'], }); expect(unusedResult).toBeUndefined(); const usedDelta = new ClientFeatureToggleDelta({ getAll: async () => [ { name: 'first', project: 'default', enabled: false, strategies: [{ name: 'default', segments: [101] }], }, ], }, createSegmentModel(), createEventStore(), { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); await usedDelta.getDelta(undefined, { environment: 'development', project: ['default'], }); currentRevisionId = 2; await usedDelta.onUpdateRevisionEvent(); const usedResult = await usedDelta.getDelta(1, { environment: 'development', project: ['default'], }); expect(usedResult).toEqual({ events: [ { eventId: 2, type: 'segment-updated', segment: { id: 101, name: 'segment-a', constraints: [] }, }, ], }); }); test('segment-removed is delivered with the feature update that dereferences it', async () => { let currentRevisionId = 1; let featureReferencesSegment = true; const delta = new ClientFeatureToggleDelta({ getAll: async () => [ { name: 'first', project: 'default', enabled: false, strategies: [ featureReferencesSegment ? { name: 'default', segments: [101] } : { name: 'default' }, ], }, ], }, { getAllForClientIds: async (ids) => ids === undefined || ids.includes(101) ? [{ id: 101, name: 'segment-a', constraints: [] }] : [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 1, segmentRevisions: new Map([[101, 1]]), }), getRevisionRange: async () => [ { id: 2, type: 'feature-updated', featureName: 'first', project: 'default', environment: 'development', createdAt: new Date(), }, { id: 3, type: SEGMENT_DELETED, preData: { id: 101, name: 'segment-a' }, createdAt: new Date(), }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); featureReferencesSegment = false; currentRevisionId = 3; await delta.onUpdateRevisionEvent(); const result = await delta.getDelta(1, { environment: 'development', project: ['default'], }); expect(result).toEqual({ events: [ { eventId: 2, type: 'feature-updated', feature: { name: 'first', project: 'default', enabled: false, strategies: [{ name: 'default' }], }, }, { eventId: 3, type: 'segment-removed', segmentId: 101, }, ], }); }); test('returns segment payload when a feature newly references a previously hidden segment update', async () => { let currentRevisionId = 7; const eventStore = { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 7]]), maxReferencedSegmentRevision: 7, segmentRevisions: new Map(), }), getRevisionRange: async (from, to) => { if (from === 7 && to === 14) { return [ { id: 12, type: 'segment-updated', data: { id: 1, name: 'segment-a' }, createdAt: new Date(), }, { id: 14, type: 'feature-updated', featureName: 'test-flag', project: 'default', environment: 'development', data: { name: 'test-flag', enabled: true }, createdAt: new Date(), }, ]; } return [ { id: 15, type: 'feature-updated', featureName: 'test-flag', project: 'default', environment: 'development', data: { name: 'test-flag', enabled: true }, createdAt: new Date(), }, ]; }, getMaxRevisionId: async () => currentRevisionId, }; const readModel = { getAll: async ({ toggleNames = [], }) => { if (toggleNames.includes('test-flag') && currentRevisionId >= 15) { return [ { name: 'test-flag', project: 'default', enabled: true, strategies: [{ name: 'default', segments: [1] }], }, ]; } return [ { name: 'test-flag', project: 'default', enabled: true, strategies: [{ name: 'default' }], }, ]; }, }; const segmentReadModel = { getAllForClientIds: async (ids) => ids?.includes(1) ? [{ id: 1, name: 'segment-a', constraints: [] }] : [], }; const delta = new ClientFeatureToggleDelta(readModel, segmentReadModel, eventStore, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); const hydration = await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); expect(hydration?.events[0]?.eventId).toBe(7); currentRevisionId = 14; await delta.onUpdateRevisionEvent(); const unrelatedUpdate = await delta.getDelta(7, { environment: 'development', project: ['default'], }); expect(unrelatedUpdate).toEqual({ events: [ { eventId: 14, type: 'feature-updated', feature: { name: 'test-flag', project: 'default', enabled: true, strategies: [{ name: 'default' }], }, }, ], }); currentRevisionId = 15; await delta.onUpdateRevisionEvent(); const newlyReferencedSegment = await delta.getDelta(14, { environment: 'development', project: ['default'], }); expect(newlyReferencedSegment).toEqual({ events: [ { eventId: 15, type: 'segment-updated', segment: { id: 1, name: 'segment-a', constraints: [] }, }, { eventId: 15, type: 'feature-updated', feature: { name: 'test-flag', project: 'default', enabled: true, strategies: [{ name: 'default', segments: [1] }], }, }, ], }); }); test('materializes delta_environment_revision_id on first hydration request', async () => { const environment = 'metric-materialization-test'; const delta = new ClientFeatureToggleDelta({ getAll: async () => [ { name: 'first', project: 'default', enabled: false, }, ], }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 7]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getMaxRevisionId: async () => 7, }, { on: () => undefined, }, {}, createDeltaConfig()); const result = await delta.getDelta(undefined, { environment, project: ['default'], }); const metrics = await register.metrics(); expect(result?.events[0]?.eventId).toBe(7); expect(metrics).toMatch(new RegExp(`delta_environment_revision_id\\{environment="${environment}"\\} 7`)); }); test('returns the same wildcard hydration revision for identical environment state across pods', async () => { const createDelta = (globalRevisionId) => new ClientFeatureToggleDelta({ getAll: async ({ environment, }) => environment === 'production' ? [ { name: 'first', project: 'default', enabled: true, }, ] : [], }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 85815]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getMaxRevisionId: async () => globalRevisionId, }, { on: () => undefined, }, {}, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); const stalePodDelta = createDelta(85815); const freshPodDelta = createDelta(85923); const stalePodResult = await stalePodDelta.getDelta(undefined, { environment: 'production', project: ['*'], }); const freshPodResult = await freshPodDelta.getDelta(undefined, { environment: 'production', project: ['*'], }); expect(stalePodResult).toBeDefined(); expect(freshPodResult).toBeDefined(); expect(stalePodResult?.events[0]?.eventId).toBe(freshPodResult?.events[0]?.eventId); }); test('returns an empty hydration event on initial request for an empty environment', async () => { const delta = new ClientFeatureToggleDelta({ getAll: async () => [], }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map(), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getMaxRevisionId: async () => 0, }, { on: () => undefined, }, {}, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); const result = await delta.getDelta(undefined, { environment: 'production', }); expect(result).toEqual({ events: [ { eventId: 0, type: 'hydration', features: [], segments: [], }, ], }); }); test('returns no delta when client explicitly requests revision 0 for an empty environment', async () => { const delta = new ClientFeatureToggleDelta({ getAll: async () => [], }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map(), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getMaxRevisionId: async () => 0, }, { on: () => undefined, }, {}, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); const result = await delta.getDelta(0, { environment: 'production', }); expect(result).toBeUndefined(); }); test('does not emit a no-op delta for an unrelated environment change', async () => { let currentRevisionId = 1; const delta = new ClientFeatureToggleDelta({ getAll: async ({ environment, toggleNames = [], }) => { const developmentFeature = { name: 'first', project: 'default', enabled: false, }; if (environment !== 'development') { return []; } if (toggleNames.length === 0) { return [developmentFeature]; } return toggleNames.includes('first') ? [developmentFeature] : []; }, }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: 'feature-updated', featureName: 'first', project: 'default', environment: 'production', }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); currentRevisionId = 2; await delta.onUpdateRevisionEvent(); const result = await delta.getDelta(1, { environment: 'development', project: ['default'], }); expect(result).toBeUndefined(); }); test('applies global events without environment to all initialized environments', async () => { let currentRevisionId = 1; const delta = new ClientFeatureToggleDelta({ getAll: async ({ environment, toggleNames = [], }) => { const featuresByEnvironment = { development: { name: 'first', project: 'default', enabled: false, }, production: { name: 'first', project: 'default', enabled: true, }, }; const feature = featuresByEnvironment[environment]; if (!feature) { return []; } if (toggleNames.length === 0) { return [feature]; } return toggleNames.includes('first') ? [feature] : []; }, }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: 'feature-updated', featureName: 'first', project: 'default', environment: null, }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); await delta.getDelta(undefined, { environment: 'production', project: ['default'], }); currentRevisionId = 2; await delta.onUpdateRevisionEvent(); const developmentResult = await delta.getDelta(1, { environment: 'development', project: ['default'], }); const productionResult = await delta.getDelta(1, { environment: 'production', project: ['default'], }); expect(developmentResult).toEqual({ events: [ { eventId: 2, type: 'feature-updated', feature: { name: 'first', project: 'default', enabled: false, }, }, ], }); expect(productionResult).toEqual({ events: [ { eventId: 2, type: 'feature-updated', feature: { name: 'first', project: 'default', enabled: true, }, }, ], }); }); test('feature project move emits feature-removed for old project and feature-updated for new project', async () => { let currentRevisionId = 1; const delta = new ClientFeatureToggleDelta({ getAll: async ({ environment, toggleNames = [], }) => { const feature = { name: 'moved-feature', project: 'new-project', enabled: true, }; if (environment !== 'development') return []; if (toggleNames.length === 0) return [feature]; return toggleNames.includes('moved-feature') ? [feature] : []; }, }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['old-project', 1]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: FEATURE_PROJECT_CHANGE, featureName: 'moved-feature', project: 'new-project', environment: null, data: { oldProject: 'old-project', newProject: 'new-project', }, }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); await delta.getDelta(undefined, { environment: 'development', project: ['*'], }); currentRevisionId = 2; await delta.onUpdateRevisionEvent(); const oldProjectResult = await delta.getDelta(1, { environment: 'development', project: ['old-project'], }); const newProjectResult = await delta.getDelta(1, { environment: 'development', project: ['new-project'], }); const bothProjectsResult = await delta.getDelta(1, { environment: 'development', project: ['old-project', 'new-project'], }); expect(oldProjectResult).toEqual({ events: [ { eventId: 2, type: 'feature-removed', featureName: 'moved-feature', project: 'old-project', }, ], }); expect(newProjectResult).toEqual({ events: [ { eventId: 2, type: 'feature-updated', feature: { name: 'moved-feature', project: 'new-project', enabled: true, }, }, ], }); expect(bothProjectsResult).toEqual({ events: [ { eventId: 2, type: 'feature-removed', featureName: 'moved-feature', project: 'old-project', }, { eventId: 2, type: 'feature-updated', feature: { name: 'moved-feature', project: 'new-project', enabled: true, }, }, ], }); }); test('bulk events pick the max revision id for the envelope', async () => { let currentRevisionId = 1; const delta = new ClientFeatureToggleDelta({ getAll: async ({ environment, toggleNames = [], }) => { const featuresByEnvironment = { development: { name: 'first', project: 'default', enabled: false, }, production: { name: 'first', project: 'default', enabled: true, }, }; const feature = featuresByEnvironment[environment]; if (!feature) { return []; } if (toggleNames.length === 0) { return [feature]; } return toggleNames.includes('first') ? [feature] : []; }, }, { getAllForClientIds: async () => [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 1]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 2, type: 'feature-updated', featureName: 'first', project: 'default', environment: null, }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, { eventBus: new EventEmitter(), getLogger: () => ({ error: () => undefined, info: () => undefined, }), }); await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); await delta.getDelta(undefined, { environment: 'production', project: ['default'], }); currentRevisionId = 2; await delta.onUpdateRevisionEvent(); const developmentResult = await delta.getDelta(1, { environment: 'development', project: ['default'], }); const productionResult = await delta.getDelta(1, { environment: 'production', project: ['default'], }); expect(developmentResult).toEqual({ events: [ { eventId: 2, type: 'feature-updated', feature: { name: 'first', project: 'default', enabled: false, }, }, ], }); expect(productionResult).toEqual({ events: [ { eventId: 2, type: 'feature-updated', feature: { name: 'first', project: 'default', enabled: true, }, }, ], }); }); test('returns delta events in revision order even when cached by event type', async () => { let currentRevisionId = 24; const delta = new ClientFeatureToggleDelta({ getAll: async ({ toggleNames = [], }) => { const feature = { name: 'first', project: 'default', enabled: false, strategies: currentRevisionId >= 26 ? [{ name: 'default', segments: [101] }] : [], }; if (toggleNames.length === 0) { return [feature]; } return toggleNames.includes('first') ? [feature] : []; }, }, { getAllForClientIds: async (ids) => ids === undefined || ids.includes(101) ? [{ id: 101, name: 'segment-a', constraints: [] }] : [], }, { getDeltaRevisionState: async () => ({ projectRevisions: new Map([['default', 24]]), maxReferencedSegmentRevision: 0, segmentRevisions: new Map(), }), getRevisionRange: async () => [ { id: 25, type: 'segment-created', data: { id: 101, name: 'segment-a' }, createdAt: new Date(), }, { id: 26, type: 'feature-updated', featureName: 'first', project: 'default', environment: 'development', createdAt: new Date(), }, ], getMaxRevisionId: async () => currentRevisionId, }, { on: () => undefined, }, { isEnabled: (name) => name === 'deltaApi', }, createDeltaConfig()); await delta.getDelta(undefined, { environment: 'development', project: ['default'], }); currentRevisionId = 26; await delta.onUpdateRevisionEvent(); const result = await delta.getDelta(24, { environment: 'development', project: ['default'], }); expect(result?.events.map((event) => event.eventId)).toEqual([25, 26]); expect(result).toEqual({ events: [ { eventId: 25, type: 'segment-updated', segment: { id: 101, name: 'segment-a', constraints: [] }, }, { eventId: 26, type: 'feature-updated', feature: { name: 'first', project: 'default', enabled: false, strategies: [{ name: 'default', segments: [101] }], }, }, ], }); }); }); //# sourceMappingURL=client-feature-toggle-delta.test.js.map