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
JavaScript
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