Skip to content

Commit 31b615e

Browse files
committed
feat: fixed-conflicts
2 parents b7cf143 + a83d3a5 commit 31b615e

2 files changed

Lines changed: 204 additions & 6 deletions

File tree

sdk/index.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export class Flagsmith {
335335
url: string,
336336
method: string,
337337
body?: { [key: string]: any }
338-
): Promise<any> {
338+
): Promise<{ response: Response; data: any }> {
339339
const headers: { [key: string]: any } = { 'Content-Type': 'application/json' };
340340
if (this.environmentKey) {
341341
headers['X-Environment-Key'] = this.environmentKey as string;
@@ -369,7 +369,7 @@ export class Flagsmith {
369369
);
370370
}
371371

372-
return data.json();
372+
return { response: data, data: await data.json() };
373373
}
374374

375375
/**
@@ -399,8 +399,52 @@ export class Flagsmith {
399399
if (!this.environmentUrl) {
400400
throw new Error('`apiUrl` argument is missing or invalid.');
401401
}
402-
const environment_data = await this.getJSONResponse(this.environmentUrl, 'GET');
403-
return buildEnvironmentModel(environment_data);
402+
const startTime = Date.now();
403+
const documents: any[] = [];
404+
let url = this.environmentUrl;
405+
let loggedWarning = false;
406+
407+
while (true) {
408+
try {
409+
if (!loggedWarning) {
410+
const elapsedMs = Date.now() - startTime;
411+
if (elapsedMs > this.environmentRefreshIntervalSeconds * 1000) {
412+
this.logger.warn(
413+
`Environment document retrieval exceeded the polling interval of ${this.environmentRefreshIntervalSeconds} seconds.`
414+
);
415+
loggedWarning = true;
416+
}
417+
}
418+
419+
const { response, data } = await this.getJSONResponse(url, 'GET');
420+
421+
documents.push(data);
422+
423+
const linkHeader = response.headers.get('link');
424+
if (linkHeader) {
425+
const nextMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
426+
427+
if (nextMatch) {
428+
const relativeUrl = decodeURIComponent(nextMatch[1]);
429+
url = new URL(relativeUrl, this.apiUrl).href;
430+
431+
continue;
432+
}
433+
}
434+
break;
435+
} catch (error) {
436+
throw error;
437+
}
438+
}
439+
440+
// Compile the document
441+
const compiledDocument = documents[0];
442+
for (let i = 1; i < documents.length; i++) {
443+
compiledDocument.identity_overrides = compiledDocument.identity_overrides || [];
444+
compiledDocument.identity_overrides.push(...(documents[i].identity_overrides || []));
445+
}
446+
447+
return buildEnvironmentModel(compiledDocument);
404448
}
405449

406450
private async getEnvironmentFlagsFromDocument(): Promise<Flags> {
@@ -456,7 +500,7 @@ export class Flagsmith {
456500
if (!this.environmentFlagsUrl) {
457501
throw new Error('`apiUrl` argument is missing or invalid.');
458502
}
459-
const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
503+
const { data: apiFlags } = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
460504
const flags = Flags.fromAPIFlags({
461505
apiFlags: apiFlags,
462506
analyticsProcessor: this.analyticsProcessor,
@@ -477,7 +521,7 @@ export class Flagsmith {
477521
throw new Error('`apiUrl` argument is missing or invalid.');
478522
}
479523
const data = generateIdentitiesData(identifier, traits, transient);
480-
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
524+
const { data: jsonResponse } = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
481525
const flags = Flags.fromAPIFlags({
482526
apiFlags: jsonResponse['flags'],
483527
analyticsProcessor: this.analyticsProcessor,

tests/sdk/flagsmith.test.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,160 @@ test('test_update_environment_sets_environment', async () => {
4040
expect(await flg.getEnvironment()).toStrictEqual(model);
4141
});
4242

43+
test('test_update_environment_handles_paginated_document', async () => {
44+
type EnvDocumentMockResponse = {
45+
responseHeader: string | null;
46+
page: any;
47+
};
48+
49+
const createMockFetch = (pages: EnvDocumentMockResponse[]) => {
50+
let callCount = 0;
51+
return vi.fn((url: string, options?: RequestInit) => {
52+
if (url.includes('/environment-document')) {
53+
const document = envDocumentMockResponse[callCount];
54+
if (document) {
55+
callCount++;
56+
57+
const responseHeaders: Record<string, string> = {};
58+
59+
if (document.responseHeader) {
60+
responseHeaders['Link'] = `<${document.responseHeader}>; rel="next"`;
61+
}
62+
63+
return Promise.resolve(
64+
new Response(JSON.stringify(document.page), {
65+
status: 200,
66+
headers: responseHeaders
67+
})
68+
);
69+
}
70+
}
71+
return Promise.resolve(new Response('unknown url ' + url, { status: 404 }));
72+
});
73+
};
74+
75+
const envDocumentMockResponse: EnvDocumentMockResponse[] = [
76+
{
77+
responseHeader: '/api/v1/environment-document?page=2',
78+
page: {
79+
id: 1,
80+
api_key: 'test-key',
81+
project: {
82+
id: 1,
83+
name: 'test',
84+
organisation: {
85+
id: 1,
86+
name: 'Test Org',
87+
feature_analytics: false,
88+
persist_trait_data: true,
89+
stop_serving_flags: false
90+
},
91+
hide_disabled_flags: false,
92+
segments: []
93+
},
94+
feature_states: [
95+
{
96+
feature_state_value: 'first_page_feature_state',
97+
multivariate_feature_state_values: [],
98+
django_id: 81027,
99+
feature: {
100+
id: 15058,
101+
type: 'STANDARD',
102+
name: 'string_feature'
103+
},
104+
enabled: false
105+
},
106+
{
107+
feature_state_value: 'second_page_feature_state',
108+
multivariate_feature_state_values: [],
109+
django_id: 81027,
110+
feature: {
111+
id: 15058,
112+
type: 'STANDARD',
113+
name: 'string_feature'
114+
},
115+
enabled: false
116+
},
117+
{
118+
feature_state_value: 'third_page_feature_state',
119+
multivariate_feature_state_values: [],
120+
django_id: 81027,
121+
feature: {
122+
id: 15058,
123+
type: 'STANDARD',
124+
name: 'string_feature'
125+
},
126+
enabled: false
127+
}
128+
],
129+
identity_overrides: [{ id: 1, identifier: 'user1' }]
130+
}
131+
},
132+
{
133+
responseHeader: '/api/v1/environment-document?page=3',
134+
page: {
135+
api_key: 'test-key',
136+
project: {
137+
id: 1,
138+
name: 'test',
139+
organisation: {
140+
id: 1,
141+
name: 'Test Org',
142+
feature_analytics: false,
143+
persist_trait_data: true,
144+
stop_serving_flags: false
145+
},
146+
hide_disabled_flags: false,
147+
segments: []
148+
},
149+
feature_states: [],
150+
identity_overrides: [{ id: 2, identifier: 'user2' }]
151+
}
152+
},
153+
{
154+
responseHeader: null,
155+
page: {
156+
api_key: 'test-key',
157+
project: {
158+
id: 1,
159+
name: 'test',
160+
organisation: {
161+
id: 1,
162+
name: 'Test Org',
163+
feature_analytics: false,
164+
persist_trait_data: true,
165+
stop_serving_flags: false
166+
},
167+
hide_disabled_flags: false,
168+
segments: []
169+
},
170+
feature_states: [],
171+
identity_overrides: [{ id: 2, identifier: 'user3' }]
172+
}
173+
}
174+
];
175+
176+
const flg = new Flagsmith({
177+
environmentKey: 'ser.key',
178+
enableLocalEvaluation: true,
179+
fetch: createMockFetch(envDocumentMockResponse)
180+
});
181+
182+
const environment = await flg.getEnvironment();
183+
184+
expect(environment.identityOverrides).toHaveLength(3);
185+
expect(environment.identityOverrides[0].identifier).toBe('user1');
186+
expect(environment.identityOverrides[1].identifier).toBe('user2');
187+
expect(environment.identityOverrides[2].identifier).toBe('user3');
188+
expect(environment.featureStates).toHaveLength(3);
189+
expect(environment.featureStates[0].getValue()).toBe('first_page_feature_state');
190+
expect(environment.featureStates[1].getValue()).toBe('second_page_feature_state');
191+
expect(environment.featureStates[2].getValue()).toBe('third_page_feature_state');
192+
expect(environment.project.name).toBe('test');
193+
expect(environment.project.organisation.name).toBe('Test Org');
194+
expect(environment.project.organisation.id).toBe(1);
195+
});
196+
43197
test('test_set_agent_options', async () => {
44198
const agent = new Agent({});
45199

0 commit comments

Comments
 (0)