diff --git a/src/dtos/types.ts b/src/dtos/types.ts index b2fc6068..aa79186b 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -204,7 +204,7 @@ export interface IExcludedSegment { export interface TargetingEntity { name: string; changeNumber: number; - status: 'ACTIVE' | 'ARCHIVED'; + status?: 'ACTIVE' | 'ARCHIVED'; conditions: IDefinitionCondition[]; } @@ -235,15 +235,17 @@ export interface IDefinition extends TargetingEntity { /** Interface of the parsed JSON response of `/splitChanges` */ export interface IDefinitionChangesResponse { - ff?: { - t: number, - s?: number, - d: IDefinition[] + d?: { + till: number, + since?: number | null, + updated: IDefinition[], + removed: string[], }, rbs?: { - t: number, - s?: number, - d: IRBSegment[] + till: number, + since?: number | null, + updated: IRBSegment[], + removed: string[], } } diff --git a/src/evaluator/matchers/matcherTypes.ts b/src/evaluator/matchers/matcherTypes.ts index 0c5faf4b..4d813382 100644 --- a/src/evaluator/matchers/matcherTypes.ts +++ b/src/evaluator/matchers/matcherTypes.ts @@ -14,7 +14,7 @@ export const matcherTypes: Record = { ENDS_WITH: 12, STARTS_WITH: 13, CONTAINS_STRING: 14, - IN_SPLIT_TREATMENT: 15, + IN_SPLIT_TREATMENT: 15, // For flags only EQUAL_TO_BOOLEAN: 16, MATCHES_STRING: 17, EQUAL_TO_SEMVER: 18, @@ -22,7 +22,7 @@ export const matcherTypes: Record = { LESS_THAN_OR_EQUAL_TO_SEMVER: 20, BETWEEN_SEMVER: 21, IN_LIST_SEMVER: 22, - IN_LARGE_SEGMENT: 23, + IN_LARGE_SEGMENT: 23, // For client-side only IN_RULE_BASED_SEGMENT: 24, }; diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index 407a35b4..9671ff8c 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -12,11 +12,11 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { protected abstract removeSplit(name: string): Promise protected abstract setChangeNumber(changeNumber: number): Promise - update(toAdd: IDefinition[], toRemove: IDefinition[], changeNumber: number): Promise { + update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): Promise { return Promise.all([ this.setChangeNumber(changeNumber), Promise.all(toAdd.map(addedFF => this.addSplit(addedFF))), - Promise.all(toRemove.map(removedFF => this.removeSplit(removedFF.name))) + Promise.all(toRemove.map(removedFF => this.removeSplit(removedFF))) ]).then(([, added, removed]) => { return added.some(result => result) || removed.some(result => result); }); diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 8c748dcc..86cc57ac 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -13,9 +13,9 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { protected abstract removeSplit(name: string): boolean protected abstract setChangeNumber(changeNumber: number): boolean | void - update(toAdd: IDefinition[], toRemove: IDefinition[], changeNumber: number): boolean { + update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): boolean { let updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result); - updated = toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; + updated = toRemove.map(removedFF => this.removeSplit(removedFF)).some(result => result) || updated; this.setChangeNumber(changeNumber); return updated; } diff --git a/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts index 2e222f32..bfa5a4e0 100644 --- a/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts @@ -30,19 +30,19 @@ describe.each([{ cache: cacheInRedis, wrapper: redisClient }, { cache: cachePlug expect(await cache.getChangeNumber()).toBe(1); // Remove a segment - expect(await cache.update([], [rbSegment], 2)).toBe(true); + expect(await cache.update([], [rbSegment.name], 2)).toBe(true); expect(await cache.get(rbSegment.name)).toBeNull(); expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); expect(await cache.getChangeNumber()).toBe(2); // Remove remaining segment - expect(await cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true); + expect(await cache.update([], [rbSegmentWithInSegmentMatcher.name], 3)).toBe(true); expect(await cache.get(rbSegment.name)).toBeNull(); expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull(); expect(await cache.getChangeNumber()).toBe(3); // No changes - expect(await cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false); + expect(await cache.update([], [rbSegmentWithInSegmentMatcher.name], 4)).toBe(false); expect(await cache.getChangeNumber()).toBe(4); }); @@ -55,6 +55,6 @@ describe.each([{ cache: cacheInRedis, wrapper: redisClient }, { cache: cachePlug expect(await cache.contains(new Set(['nonexistent']))).toBe(false); expect(await cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); - await cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2); + await cache.update([], [rbSegment.name, rbSegmentWithInSegmentMatcher.name], 2); }); }); diff --git a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts index 0946bacf..5dc76821 100644 --- a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts @@ -33,19 +33,19 @@ describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Me expect(cache.getChangeNumber()).toBe(1); // Remove a segment - expect(cache.update([], [rbSegment], 2)).toBe(true); + expect(cache.update([], [rbSegment.name], 2)).toBe(true); expect(cache.get(rbSegment.name)).toBeNull(); expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); expect(cache.getChangeNumber()).toBe(2); // Remove remaining segment - expect(cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true); + expect(cache.update([], [rbSegmentWithInSegmentMatcher.name], 3)).toBe(true); expect(cache.get(rbSegment.name)).toBeNull(); expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull(); expect(cache.getChangeNumber()).toBe(3); // No changes - expect(cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false); + expect(cache.update([], [rbSegmentWithInSegmentMatcher.name], 4)).toBe(false); expect(cache.getChangeNumber()).toBe(4); }); @@ -58,7 +58,7 @@ describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Me expect(cache.contains(new Set(['nonexistent']))).toBe(false); expect(cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); - cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2); + cache.update([], [rbSegment.name, rbSegmentWithInSegmentMatcher.name], 2); }); test('usesSegments should track segments usage correctly', () => { diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts index 61bf13fa..c5601987 100644 --- a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -25,9 +25,9 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { this.storage.removeItem(this.keys.buildRBSegmentsTillKey()); } - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): boolean { let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); - updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + updated = toRemove.map(toRemove => this.remove(toRemove)).some(result => result) || updated; this.setChangeNumber(changeNumber); return updated; } diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index c8c79c5e..9a88265f 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -54,7 +54,7 @@ describe.each(storages)('SPLITS CACHE', (storage) => { cache.update([something, somethingElse], [], 1); - cache.update([], [something, somethingElse], 1); + cache.update([], [something.name, somethingElse.name], 1); expect(cache.getSplit(something.name)).toBe(null); expect(cache.getSplit(somethingElse.name)).toBe(null); diff --git a/src/storages/inMemory/RBSegmentsCacheInMemory.ts b/src/storages/inMemory/RBSegmentsCacheInMemory.ts index cd98dc62..995ed46a 100644 --- a/src/storages/inMemory/RBSegmentsCacheInMemory.ts +++ b/src/storages/inMemory/RBSegmentsCacheInMemory.ts @@ -15,9 +15,9 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync { this.segmentsCount = 0; } - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): boolean { let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); - updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + updated = toRemove.map(toRemove => this.remove(toRemove)).some(result => result) || updated; this.changeNumber = changeNumber; return updated; } diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index f755a295..56e002d3 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -46,7 +46,7 @@ test('SPLITS CACHE / In Memory / Update Splits', () => { cache.update([something, somethingElse], [], 1); - cache.update([], [something, somethingElse], 1); + cache.update([], [something.name, somethingElse.name], 1); expect(cache.getSplit(something.name)).toBe(null); expect(cache.getSplit(somethingElse.name)).toBe(null); diff --git a/src/storages/inRedis/RBSegmentsCacheInRedis.ts b/src/storages/inRedis/RBSegmentsCacheInRedis.ts index e708dd49..6b1c66ad 100644 --- a/src/storages/inRedis/RBSegmentsCacheInRedis.ts +++ b/src/storages/inRedis/RBSegmentsCacheInRedis.ts @@ -37,7 +37,7 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { }); } - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): Promise { return Promise.all([ this.setChangeNumber(changeNumber), Promise.all(toAdd.map(toAdd => { @@ -46,7 +46,7 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { return this.redis.set(key, stringifiedNewRBSegment).then(() => true); })), Promise.all(toRemove.map(toRemove => { - const key = this.keys.buildRBSegmentKey(toRemove.name); + const key = this.keys.buildRBSegmentKey(toRemove); return this.redis.del(key).then((status: number) => status === 1); })) ]).then(([, added, removed]) => { diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index d42143ff..06b3edfe 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -138,7 +138,7 @@ describe('SPLITS CACHE REDIS', () => { expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older // Delete splits and TT keys - await cache.update([], [splitWithUserTT, splitWithAccountTT], -1); + await cache.update([], [splitWithUserTT.name, splitWithAccountTT.name], -1); await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); @@ -191,7 +191,7 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getNamesByFlagSets([])).toEqual([]); // Delete splits, TT and flag set keys - await cache.update([], [featureFlagThree, featureFlagTwo, featureFlagWithEmptyFS], -1); + await cache.update([], [featureFlagThree.name, featureFlagTwo.name, featureFlagWithEmptyFS.name], -1); await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); @@ -219,7 +219,7 @@ describe('SPLITS CACHE REDIS', () => { expect(await cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); // Delete splits, TT and flag set keys - await cacheWithoutFilters.update([], [featureFlagThree, featureFlagTwo, featureFlagOne, featureFlagWithEmptyFS], -1); + await cacheWithoutFilters.update([], [featureFlagThree.name, featureFlagTwo.name, featureFlagOne.name, featureFlagWithEmptyFS.name], -1); await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); diff --git a/src/storages/pluggable/RBSegmentsCachePluggable.ts b/src/storages/pluggable/RBSegmentsCachePluggable.ts index c1967f6d..bf2ba81d 100644 --- a/src/storages/pluggable/RBSegmentsCachePluggable.ts +++ b/src/storages/pluggable/RBSegmentsCachePluggable.ts @@ -36,7 +36,7 @@ export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { }); } - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): Promise { return Promise.all([ this.setChangeNumber(changeNumber), Promise.all(toAdd.map(toAdd => { @@ -45,7 +45,7 @@ export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { return this.wrapper.set(key, stringifiedNewRBSegment).then(() => true); })), Promise.all(toRemove.map(toRemove => { - const key = this.keys.buildRBSegmentKey(toRemove.name); + const key = this.keys.buildRBSegmentKey(toRemove); return this.wrapper.del(key); })) ]).then(([, added, removed]) => { diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index 8f9c966c..55b063b5 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -130,7 +130,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older // Delete splits and TT keys - await cache.update([], [splitWithUserTT, splitWithAccountTT], -1); + await cache.update([], [splitWithUserTT.name, splitWithAccountTT.name], -1); await wrapper.del(keysBuilder.buildSplitsTillKey()); expect(await wrapper.getKeysByPrefix('SPLITIO')).toHaveLength(0); }); diff --git a/src/storages/types.ts b/src/storages/types.ts index 222106d7..d5b98f97 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,5 +1,6 @@ import SplitIO from '../../types/splitio'; -import { MaybeThenable, IDefinition, IRBSegment, IMySegmentsResponse, IMembershipsResponse, ISegmentChangesResponse, IDefinitionChangesResponse } from '../dtos/types'; +import { MaybeThenable, IDefinition, IRBSegment, IMySegmentsResponse, IMembershipsResponse, ISegmentChangesResponse } from '../dtos/types'; +import { ISplitChangesResponse } from '../sync/polling/fetchers/splitChangesFetcher'; import { MySegmentsData } from '../sync/polling/types'; import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types'; import { ISettings } from '../types'; @@ -194,7 +195,7 @@ export interface IPluggableStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - update(toAdd: IDefinition[], toRemove: IDefinition[], changeNumber: number): MaybeThenable, + update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec // should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage. @@ -211,7 +212,7 @@ export interface ISplitsCacheBase { } export interface ISplitsCacheSync extends ISplitsCacheBase { - update(toAdd: IDefinition[], toRemove: IDefinition[], changeNumber: number): boolean, + update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): boolean, getSplit(name: string): IDefinition | null, getSplits(names: string[]): Record, getChangeNumber(): number, @@ -225,7 +226,7 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { } export interface ISplitsCacheAsync extends ISplitsCacheBase { - update(toAdd: IDefinition[], toRemove: IDefinition[], changeNumber: number): Promise, + update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, getChangeNumber(): Promise, @@ -241,7 +242,7 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { /** Rule-Based Segments cache */ export interface IRBSegmentsCacheBase { - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): MaybeThenable, + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): MaybeThenable, get(name: string): MaybeThenable, getChangeNumber(): MaybeThenable, clear(): MaybeThenable, @@ -249,7 +250,7 @@ export interface IRBSegmentsCacheBase { } export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase { - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean, + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): boolean, get(name: string): IRBSegment | null, getChangeNumber(): number, getAll(): IRBSegment[], @@ -260,7 +261,7 @@ export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase { } export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase { - update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise, + update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): Promise, get(name: string): Promise, getChangeNumber(): Promise, clear(): Promise, @@ -544,7 +545,7 @@ export type RolloutPlan = { /** * Feature flags and rule-based segments. */ - splitChanges: IDefinitionChangesResponse; + splitChanges: ISplitChangesResponse; /** * Optional map of matching keys to their memberships. */ diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 5f677a0a..7635c5d3 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,5 +1,5 @@ import { ISettings } from '../../../types'; -import { IDefinitionChangesResponse } from '../../../dtos/types'; +import { IDefinition, IRBSegment, IDefinitionChangesResponse } from '../../../dtos/types'; import { IResponse } from '../../../services/types'; import { FLAG_SPEC_VERSION } from '../../../utils/constants'; import { base } from '../../../utils/settingsValidation'; @@ -15,6 +15,39 @@ function sdkEndpointOverridden(settings: ISettings) { return settings.urls.sdk !== base.urls.sdk; } +export type ISplit = IDefinition; + +/** JSON response of `/splitChanges` */ +export interface ISplitChangesResponse { + ff?: { + t: number, + s?: number, + d: ISplit[] + }, + rbs?: { + t: number, + s?: number, + d: IRBSegment[] + } +} + +/** JSON response of `/splitChanges` for flag spec version 1.2 or below */ +export interface ISplitChangesLegacyResponse { + till: number, + since?: number, + splits: ISplit[] +} + +function isSplitChangesLegacyResponse(data: ISplitChangesResponse | ISplitChangesLegacyResponse): data is ISplitChangesLegacyResponse { + return (data as ISplitChangesLegacyResponse).splits != null; +} + +function partitionByStatus(items: T[], till: number, since?: number) { + const updated: T[] = [], removed: string[] = []; + items.forEach(item => item.status === 'ARCHIVED' ? removed.push(item.name) : updated.push(item)); + return { updated, removed, till, since }; +} + /** * Factory of SplitChanges fetcher. * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors. @@ -57,16 +90,10 @@ export function splitChangesFetcherFactory(params: ISdkFactoryContextSync): IDef return splitsPromise .then(resp => resp.json()) - .then(data => { + .then((data: ISplitChangesResponse | ISplitChangesLegacyResponse) => { // Using flag spec version 1.2 or below - if (data.splits) { - return { - ff: { - d: data.splits, - s: data.since, - t: data.till - } - }; + if (isSplitChangesLegacyResponse(data)) { + return { d: partitionByStatus(data.splits || [], data.till, data.since) }; } // Proxy recovery @@ -80,7 +107,12 @@ export function splitChangesFetcherFactory(params: ISdkFactoryContextSync): IDef ); } - return data; + const { ff, rbs } = data; + + return { + d: ff?.d ? partitionByStatus(ff.d, ff.t, ff.s) : undefined, + rbs: rbs?.d ? partitionByStatus(rbs.d, rbs.t, rbs.s) : undefined, + }; }); } diff --git a/src/sync/polling/updaters/__tests__/definitionChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/definitionChangesUpdater.spec.ts index 757458f5..2b119679 100644 --- a/src/sync/polling/updaters/__tests__/definitionChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/definitionChangesUpdater.spec.ts @@ -1,4 +1,4 @@ -import { IRBSegment, IDefinition } from '../../../../dtos/types'; +import { IRBSegment, IDefinition, IDefinitionCondition } from '../../../../dtos/types'; import { readinessManagerFactory } from '../../../../readiness/readinessManager'; import { splitApiFactory } from '../../../../services/splitApi'; import { SegmentsCacheInMemory } from '../../../../storages/inMemory/SegmentsCacheInMemory'; @@ -38,59 +38,54 @@ const activeSplitWithSegments = { }] } }] -}; +} as IDefinition; const archivedSplit = { name: 'Split2', status: 'ARCHIVED' -}; -// @ts-ignore -const testFFSetsAB: IDefinition = -{ +} as IDefinition; + +const testFFSetsAB = { name: 'test', status: 'ACTIVE', - conditions: [], + conditions: [] as IDefinitionCondition[], killed: false, sets: ['set_a', 'set_b'] -}; -// @ts-ignore -const test2FFSetsX: IDefinition = -{ +} as IDefinition; + +const test2FFSetsX = { name: 'test2', status: 'ACTIVE', - conditions: [], + conditions: [] as IDefinitionCondition[], killed: false, sets: ['set_x'] -}; -// @ts-ignore -const testFFRemoveSetB: IDefinition = -{ +} as IDefinition; + +const testFFRemoveSetB = { name: 'test', status: 'ACTIVE', - conditions: [], + conditions: [] as IDefinitionCondition[], sets: ['set_a'] -}; -// @ts-ignore -const testFFRemoveSetA: IDefinition = -{ +} as IDefinition; + +const testFFRemoveSetA = { name: 'test', status: 'ACTIVE', - conditions: [], + conditions: [] as IDefinitionCondition[], sets: ['set_x'] -}; -// @ts-ignore -const testFFEmptySet: IDefinition = -{ +} as IDefinition; + +const testFFEmptySet = { name: 'test', status: 'ACTIVE', - conditions: [], - sets: [] -}; -// @ts-ignore -const rbsWithExcludedSegment: IRBSegment = { + conditions: [] as IDefinitionCondition[], + sets: [] as string[] +} as IDefinition; + +const rbsWithExcludedSegment = { name: 'rbs', status: 'ACTIVE', - conditions: [], + conditions: [] as IDefinitionCondition[], excluded: { segments: [{ type: 'standard', @@ -100,7 +95,7 @@ const rbsWithExcludedSegment: IRBSegment = { name: 'D' }] } -}; +} as IRBSegment; test('definitionChangesUpdater / segments parser', () => { let segments = parseSegments(activeSplitWithSegments as IDefinition); @@ -117,17 +112,17 @@ test('definitionChangesUpdater / compute splits mutation', () => { const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; let segments = new Set(); - let splitsMutation = computeMutation([activeSplitWithSegments, archivedSplit] as IDefinition[], segments, splitFiltersValidation); + let splitsMutation = computeMutation({ updated: [activeSplitWithSegments], removed: [archivedSplit.name] }, segments, splitFiltersValidation); expect(splitsMutation.added).toEqual([activeSplitWithSegments]); - expect(splitsMutation.removed).toEqual([archivedSplit]); - expect(splitsMutation.names).toEqual([activeSplitWithSegments.name, archivedSplit.name]); + expect(splitsMutation.removed).toEqual([archivedSplit.name]); + expect(splitsMutation.names).toEqual([archivedSplit.name, activeSplitWithSegments.name]); expect(Array.from(segments)).toEqual(['A', 'B']); // SDK initialization without sets // should process all the notifications segments = new Set(); - splitsMutation = computeMutation([testFFSetsAB, test2FFSetsX] as IDefinition[], segments, splitFiltersValidation); + splitsMutation = computeMutation({ updated: [testFFSetsAB, test2FFSetsX], removed: [] }, segments, splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFSetsAB, test2FFSetsX]); expect(splitsMutation.removed).toEqual([]); @@ -140,7 +135,7 @@ test('definitionChangesUpdater / compute splits mutation with filters', () => { let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a', 'set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; // fetching new feature flag in sets A & B - let splitsMutation = computeMutation([testFFSetsAB], new Set(), splitFiltersValidation); + let splitsMutation = computeMutation({ updated: [testFFSetsAB], removed: [] }, new Set(), splitFiltersValidation); // should add it to mutations expect(splitsMutation.added).toEqual([testFFSetsAB]); @@ -148,38 +143,38 @@ test('definitionChangesUpdater / compute splits mutation with filters', () => { expect(splitsMutation.names).toEqual([testFFSetsAB.name]); // fetching existing test feature flag removed from set B - splitsMutation = computeMutation([testFFRemoveSetB], new Set(), splitFiltersValidation); + splitsMutation = computeMutation({ updated: [testFFRemoveSetB], removed: [] }, new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFRemoveSetB]); expect(splitsMutation.removed).toEqual([]); expect(splitsMutation.names).toEqual([testFFRemoveSetB.name]); // fetching existing test feature flag removed from set B - splitsMutation = computeMutation([testFFRemoveSetA], new Set(), splitFiltersValidation); + splitsMutation = computeMutation({ updated: [testFFRemoveSetA], removed: [] }, new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFRemoveSetA]); + expect(splitsMutation.removed).toEqual([testFFRemoveSetA.name]); expect(splitsMutation.names).toEqual([testFFRemoveSetA.name]); // fetching existing test feature flag removed from set B - splitsMutation = computeMutation([testFFEmptySet], new Set(), splitFiltersValidation); + splitsMutation = computeMutation({ updated: [testFFEmptySet], removed: [] }, new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFEmptySet]); + expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); expect(splitsMutation.names).toEqual([testFFEmptySet.name]); // SDK initialization with names: ['test2'] splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; - splitsMutation = computeMutation([testFFSetsAB], new Set(), splitFiltersValidation); + splitsMutation = computeMutation({ updated: [testFFSetsAB], removed: [] }, new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFSetsAB]); + expect(splitsMutation.removed).toEqual([testFFSetsAB.name]); expect(splitsMutation.names).toEqual([testFFSetsAB.name]); - splitsMutation = computeMutation([test2FFSetsX, testFFEmptySet], new Set(), splitFiltersValidation); + splitsMutation = computeMutation({ updated: [test2FFSetsX, testFFEmptySet], removed: [] }, new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([test2FFSetsX]); - expect(splitsMutation.removed).toEqual([testFFEmptySet]); + expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); expect(splitsMutation.names).toEqual([test2FFSetsX.name, testFFEmptySet.name]); }); @@ -243,7 +238,7 @@ describe('definitionChangesUpdater', () => { // Add feature flag in notification expect(updateSplits.mock.calls[index][0].length).toBe(payload.status === ARCHIVED_FF ? 0 : 1); // Remove feature flag if status is ARCHIVED - expect(updateSplits.mock.calls[index][1]).toEqual(payload.status === ARCHIVED_FF ? [payload] : []); + expect(updateSplits.mock.calls[index][1]).toEqual(payload.status === ARCHIVED_FF ? [payload.name] : []); // fetch segments after feature flag update expect(registerSegments).toBeCalledTimes(index + 1); expect(registerSegments.mock.calls[index][0]).toEqual(payload.status === ARCHIVED_FF ? [] : ['maur-2']); @@ -268,6 +263,22 @@ describe('definitionChangesUpdater', () => { expect(registerSegments).toBeCalledWith([]); }); + test('test with archived rbsegment payload', async () => { + const payload = { name: 'rbsegment', status: 'ARCHIVED', changeNumber: 1684329854386, conditions: [] } as unknown as IRBSegment; + const changeNumber = payload.changeNumber; + + await expect(definitionChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: RB_SEGMENT_UPDATE })).resolves.toBe(true); + + expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(0); + + expect(updateRbSegments).toBeCalledTimes(1); + expect(updateRbSegments).toBeCalledWith([], [payload.name], changeNumber); + + expect(registerSegments).toBeCalledTimes(1); + expect(registerSegments).toBeCalledWith([]); + }); + test('flag sets splits-arrived emission', async () => { const payload = splitNotifications[3].decoded as Pick; const setMocks = [ diff --git a/src/sync/polling/updaters/definitionChangesUpdater.ts b/src/sync/polling/updaters/definitionChangesUpdater.ts index d7587eff..6653d585 100644 --- a/src/sync/polling/updaters/definitionChangesUpdater.ts +++ b/src/sync/polling/updaters/definitionChangesUpdater.ts @@ -11,8 +11,9 @@ import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT import { setToArray } from '../../../utils/lang/sets'; import { SPLIT_UPDATE } from '../../streaming/constants'; import { SdkUpdateMetadata } from '../../../../types/splitio'; +import { ISplit } from '../fetchers/splitChangesFetcher'; -export type InstantUpdate = { payload: IDefinition | IRBSegment, changeNumber: number, type: string }; +export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; type DefinitionChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment). @@ -57,7 +58,7 @@ export function parseSegments(ruleEntity: IDefinition | IRBSegment, matcherType: interface IDefinitionMutations { added: T[], - removed: T[], + removed: string[], names: string[] } @@ -87,22 +88,27 @@ function matchFilters(definition: IDefinition, filters: ISplitFiltersValidation) * i.e., an object with added definitions, removed definitions, and used segments. * Exported for testing purposes. */ -export function computeMutation(rules: Array, segments: Set, filters?: ISplitFiltersValidation): IDefinitionMutations { - - return rules.reduce((accum, ruleEntity) => { - if (ruleEntity.status !== 'ARCHIVED' && (!filters || matchFilters(ruleEntity as IDefinition, filters))) { +export function computeMutation(update: { updated: T[], removed: string[] }, segments: Set, filters?: ISplitFiltersValidation): IDefinitionMutations { + return update.updated.reduce((accum, ruleEntity) => { + if (!filters || matchFilters(ruleEntity as IDefinition, filters)) { accum.added.push(ruleEntity); parseSegments(ruleEntity).forEach((segmentName: string) => { segments.add(segmentName); }); } else { - accum.removed.push(ruleEntity); + accum.removed.push(ruleEntity.name); } accum.names.push(ruleEntity.name); - return accum; - }, { added: [], removed: [], names: [] } as IDefinitionMutations); + }, { added: [], removed: update.removed, names: update.removed.slice() } as IDefinitionMutations); +} + +function convertInstantUpdateToDefinitionChanges(instantUpdate: InstantUpdate) { + const { payload, changeNumber } = instantUpdate; + return payload.status === 'ARCHIVED' ? + { till: changeNumber, updated: [], removed: [payload.name] } : + { till: changeNumber, updated: [payload], removed: [] }; } /** @@ -155,16 +161,17 @@ export function definitionChangesUpdaterFactory( function _definitionChangesUpdater(sinces: [number, number], retry = 0): Promise { const [since, rbSince] = sinces; log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]); + return Promise.resolve( instantUpdate ? instantUpdate.type === SPLIT_UPDATE ? // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { return contains ? - { ff: { d: [instantUpdate.payload as IDefinition], t: instantUpdate.changeNumber } } : + { d: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['d'] } : definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator); }) : - { rbs: { d: [instantUpdate.payload as IRBSegment], t: instantUpdate.changeNumber } } : + { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } : definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((definitionChanges: IDefinitionChangesResponse) => { @@ -172,18 +179,18 @@ export function definitionChangesUpdaterFactory( let updatedDefinitions: string[] = []; let ffUpdate: MaybeThenable = false; - if (definitionChanges.ff) { - const { added, removed, names } = computeMutation(definitionChanges.ff.d, usedSegments, splitFiltersValidation); + if (definitionChanges.d) { + const { added, removed, names } = computeMutation(definitionChanges.d, usedSegments, splitFiltersValidation); updatedDefinitions = names; log.debug(SYNC_UPDATE, [definitionChangesFetcher.type, added.length, removed.length]); - ffUpdate = splits.update(added, removed, definitionChanges.ff.t); + ffUpdate = splits.update(added, removed, definitionChanges.d.till); } let rbsUpdate: MaybeThenable = false; if (definitionChanges.rbs) { - const { added, removed } = computeMutation(definitionChanges.rbs.d, usedSegments); + const { added, removed } = computeMutation(definitionChanges.rbs, usedSegments); log.debug(SYNC_UPDATE, ['rule-based segments', added.length, removed.length]); - rbsUpdate = rbSegments.update(added, removed, definitionChanges.rbs.t); + rbsUpdate = rbSegments.update(added, removed, definitionChanges.rbs.till); } return Promise.all([ffUpdate, rbsUpdate,