Skip to content

Commit 7f04526

Browse files
Merge pull request #854 from splitio/SDKS-9171_sdk_ready_from_cache
[SDKS-9171] Emit SDK_READY_FROM_CACHE event alongside SDK_READY event in case it has not been emitted
2 parents d072826 + e19bb21 commit 7f04526

File tree

2 files changed

+48
-49
lines changed

2 files changed

+48
-49
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- Added two new configuration options for the SDK's `LOCALSTORAGE` storage type to control the behavior of the persisted rollout plan cache in the browser:
33
- `storage.expirationDays` to specify the validity period of the rollout plan cache in days.
44
- `storage.clearOnInit` to clear the rollout plan cache on SDK initialization.
5+
- Updated SDK_READY_FROM_CACHE event when using the `LOCALSTORAGE` storage type to be emitted alongside the SDK_READY event if it has not already been emitted.
56

67
11.1.0 (January 17, 2025)
78
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs.

src/__tests__/browserSuites/ready-from-cache.spec.js

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export default function (fetchMock, assert) {
9595
events: 'https://events.baseurl/readyFromCacheEmpty'
9696
};
9797
localStorage.clear();
98-
t.plan(3);
98+
t.plan(4);
9999

100100
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: splitChangesMock1 });
101101
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 });
@@ -124,18 +124,17 @@ export default function (fetchMock, assert) {
124124
t.end();
125125
});
126126
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
127-
t.fail('It should not emit SDK_READY_FROM_CACHE if there is no cache.');
128-
t.end();
127+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY');
129128
});
130129

131130
client.on(client.Event.SDK_READY, () => {
132-
t.pass('It should emit SDK_READY alone, since there was no cache.');
131+
t.true(client.__getStatus().isReadyFromCache, 'Client should emit SDK_READY and it should be ready from cache');
133132
});
134133
client2.on(client.Event.SDK_READY, () => {
135-
t.pass('It should emit SDK_READY alone, since there was no cache.');
134+
t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache');
136135
});
137136
client3.on(client.Event.SDK_READY, () => {
138-
t.pass('It should emit SDK_READY alone, since there was no cache.');
137+
t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache');
139138
});
140139

141140
});
@@ -148,17 +147,17 @@ export default function (fetchMock, assert) {
148147
localStorage.clear();
149148
t.plan(12 * 2 + 3);
150149

151-
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', function () {
150+
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', () => {
152151
return new Promise(res => { setTimeout(() => res({ status: 200, body: { ...splitChangesMock1, since: 25 }, headers: {} }), 200); }); // 400ms is how long it'll take to reply with Splits, no SDK_READY should be emitted before that.
153152
});
154153
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 });
155-
fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', function () {
154+
fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', () => {
156155
return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), 400); }); // First client gets segments before splits. No segment cache loading (yet)
157156
});
158-
fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', function () {
157+
fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', () => {
159158
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 700); }); // Second client gets segments after 700ms
160159
});
161-
fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', function () {
160+
fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', () => {
162161
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 1000); }); // Third client memberships will come after 1s
163162
});
164163
fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', 200);
@@ -255,18 +254,18 @@ export default function (fetchMock, assert) {
255254
localStorage.clear();
256255
t.plan(12 * 2 + 5);
257256

258-
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', function () {
257+
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', () => {
259258
t.equal(localStorage.getItem('readyFromCache_3.SPLITIO.split.always_on'), alwaysOnSplitInverted, 'feature flags must not be cleaned from cache');
260259
return new Promise(res => { setTimeout(() => res({ status: 200, body: { ...splitChangesMock1, since: 25 }, headers: {} }), 200); }); // 400ms is how long it'll take to reply with Splits, no SDK_READY should be emitted before that.
261260
});
262261
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 });
263-
fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', function () {
262+
fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', () => {
264263
return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), 400); }); // First client gets segments before splits. No segment cache loading (yet)
265264
});
266-
fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', function () {
265+
fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', () => {
267266
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 700); }); // Second client gets segments after 700ms
268267
});
269-
fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', function () {
268+
fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', () => {
270269
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 1000); }); // Third client memberships will come after 1s
271270
});
272271
fetchMock.get(testUrls.sdk + '/memberships/nicolas4%40split.io', { 'ms': {} });
@@ -365,28 +364,30 @@ export default function (fetchMock, assert) {
365364
});
366365

367366
assert.test(t => { // Testing when we start with cached data but expired (lastUpdate timestamp lower than custom (1) expirationDays ago)
367+
const CLIENT_READY_MS = 400, CLIENT2_READY_MS = 700, CLIENT3_READY_MS = 1000;
368+
368369
const testUrls = {
369370
sdk: 'https://sdk.baseurl/readyFromCacheWithData4',
370371
events: 'https://events.baseurl/readyFromCacheWithData4'
371372
};
372373
localStorage.clear();
373374

374-
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=-1', function () {
375+
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=-1', () => {
375376
t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed');
376377
t.equal(localStorage.getItem('readyFromCache_4.SPLITIO.hash'), expectedHashNullFilter, 'storage hash must not be changed');
377378
t.true(nearlyEqual(parseInt(localStorage.getItem('readyFromCache_4.SPLITIO.lastClear'), 10), Date.now()), 'storage lastClear timestamp must be updated');
378379
t.equal(localStorage.length, 3, 'feature flags cache data must be cleaned from localStorage');
379380
return { status: 200, body: splitChangesMock1 };
380381
});
381382
fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 });
382-
fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', function () {
383-
return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), 400); }); // First client gets segments before splits. No segment cache loading (yet)
383+
fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', () => {
384+
return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), CLIENT_READY_MS); }); // First client gets segments before splits. No segment cache loading (yet)
384385
});
385-
fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', function () {
386-
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 700); }); // Second client gets segments after 700ms
386+
fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', () => {
387+
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), CLIENT2_READY_MS); }); // Second client gets segments after 700ms
387388
});
388-
fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', function () {
389-
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 1000); }); // Third client memberships will come after 1s
389+
fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', () => {
390+
return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), CLIENT3_READY_MS); }); // Third client memberships will come after 1s
390391
});
391392
fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', 200);
392393
fetchMock.postOnce(testUrls.events + '/testImpressions/count', 200);
@@ -423,37 +424,34 @@ export default function (fetchMock, assert) {
423424
});
424425

425426
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
426-
t.fail('It should not emit SDK_READY_FROM_CACHE if there is expired cache.');
427-
t.end();
427+
t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should emit SDK_READY_FROM_CACHE alongside SDK_READY');
428428
});
429429
client2.once(client2.Event.SDK_READY_FROM_CACHE, () => {
430-
t.fail('It should not emit SDK_READY_FROM_CACHE if there is expired cache.');
431-
t.end();
430+
t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should emit SDK_READY_FROM_CACHE alongside SDK_READY');
432431
});
433432
client3.once(client3.Event.SDK_READY_FROM_CACHE, () => {
434-
t.fail('It should not emit SDK_READY_FROM_CACHE if there is expired cache.');
435-
t.end();
433+
t.true(nearlyEqual(Date.now() - startTime, CLIENT3_READY_MS), 'It should emit SDK_READY_FROM_CACHE alongside SDK_READY');
436434
});
437435

438436
client.on(client.Event.SDK_READY, () => {
439-
t.true(Date.now() - startTime >= 400, 'It should emit SDK_READY after syncing with the cloud.');
437+
t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should emit SDK_READY after syncing with the cloud.');
440438
t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.');
441439
});
442440
client.ready().then(() => {
443-
t.true(Date.now() - startTime >= 400, 'It should resolve ready promise after syncing with the cloud.');
441+
t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should resolve ready promise after syncing with the cloud.');
444442
t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.');
445443
});
446444
client2.on(client2.Event.SDK_READY, () => {
447-
t.true(Date.now() - startTime >= 700, 'It should emit SDK_READY after syncing with the cloud.');
445+
t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should emit SDK_READY after syncing with the cloud.');
448446
t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.');
449447
});
450448
client2.ready().then(() => {
451-
t.true(Date.now() - startTime >= 700, 'It should resolve ready promise after syncing with the cloud.');
449+
t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should resolve ready promise after syncing with the cloud.');
452450
t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.');
453451
});
454452
client3.on(client3.Event.SDK_READY, () => {
455453
client3.ready().then(() => {
456-
t.true(Date.now() - startTime >= 1000, 'It should resolve ready promise after syncing with the cloud.');
454+
t.true(nearlyEqual(Date.now() - startTime, CLIENT3_READY_MS), 'It should resolve ready promise after syncing with the cloud.');
457455
t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.');
458456

459457
// Last cb: destroy clients and check that localstorage has the expected items
@@ -486,7 +484,7 @@ export default function (fetchMock, assert) {
486484
events: 'https://events.baseurl/readyFromCache_5'
487485
};
488486
localStorage.clear();
489-
t.plan(7);
487+
t.plan(8);
490488

491489
fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE
492490
fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } });
@@ -512,8 +510,7 @@ export default function (fetchMock, assert) {
512510
const manager = splitio.manager();
513511

514512
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
515-
t.fail('It should not emit SDK_READY_FROM_CACHE because localStorage is cleaned and there isn\'t cached feature flags');
516-
t.end();
513+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY');
517514
});
518515

519516
client.once(client.Event.SDK_READY, () => {
@@ -537,7 +534,7 @@ export default function (fetchMock, assert) {
537534
events: 'https://events.baseurl/readyFromCache_5B'
538535
};
539536
localStorage.clear();
540-
t.plan(5);
537+
t.plan(6);
541538

542539
fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE
543540
fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } });
@@ -557,8 +554,7 @@ export default function (fetchMock, assert) {
557554
const manager = splitio.manager();
558555

559556
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
560-
t.fail('It should not emit SDK_READY_FROM_CACHE if cache is empty.');
561-
t.end();
557+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY');
562558
});
563559

564560
client.once(client.Event.SDK_READY, () => {
@@ -630,7 +626,7 @@ export default function (fetchMock, assert) {
630626
events: 'https://events.baseurl/readyFromCache_7'
631627
};
632628
localStorage.clear();
633-
t.plan(6);
629+
t.plan(7);
634630

635631
fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1&prefixes=p1,p2', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE
636632
fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } });
@@ -659,8 +655,7 @@ export default function (fetchMock, assert) {
659655
const manager = splitio.manager();
660656

661657
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
662-
t.fail('It should not emit SDK_READY_FROM_CACHE if cache has expired.');
663-
t.end();
658+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY');
664659
});
665660

666661
client.once(client.Event.SDK_READY, () => {
@@ -696,7 +691,7 @@ export default function (fetchMock, assert) {
696691
events: 'https://events.baseurl/readyFromCache_8'
697692
};
698693
localStorage.clear();
699-
t.plan(7);
694+
t.plan(8);
700695

701696
fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split, splitDeclarations.p3__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE
702697
fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } });
@@ -721,8 +716,7 @@ export default function (fetchMock, assert) {
721716
const manager = splitio.manager();
722717

723718
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
724-
t.fail('It should not emit SDK_READY_FROM_CACHE because all feature flags were removed from cache since the filter query changed.');
725-
t.end();
719+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY');
726720
});
727721

728722
client.once(client.Event.SDK_READY, () => {
@@ -823,10 +817,12 @@ export default function (fetchMock, assert) {
823817

824818
t.true(console.log.calledWithMatch('clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'), 'It should log a message about cleaning up cache');
825819

826-
client.once(client.Event.SDK_READY_FROM_CACHE, () => t.fail('It should not emit SDK_READY_FROM_CACHE because clearOnInit is true.'));
820+
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
821+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true');
822+
});
827823

828824
await client.ready();
829-
t.equal(manager.names().sort().length, 32, 'active splits should be present for evaluation');
825+
t.equal(manager.names().sort().length, 33, 'active splits should be present for evaluation');
830826

831827
await splitio.destroy();
832828
t.equal(localStorage.getItem('readyFromCache_10.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits');
@@ -843,7 +839,7 @@ export default function (fetchMock, assert) {
843839

844840
await new Promise(res => client.once(client.Event.SDK_READY_FROM_CACHE, res));
845841

846-
t.equal(manager.names().sort().length, 32, 'active splits should be present for evaluation');
842+
t.equal(manager.names().sort().length, 33, 'active splits should be present for evaluation');
847843
t.false(console.log.calledWithMatch('clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'), 'It should log a message about cleaning up cache');
848844

849845
await splitio.destroy();
@@ -858,11 +854,13 @@ export default function (fetchMock, assert) {
858854
client = splitio.client();
859855
manager = splitio.manager();
860856

861-
client.once(client.Event.SDK_READY_FROM_CACHE, () => t.fail('It should not emit SDK_READY_FROM_CACHE because clearOnInit is true.'));
857+
client.once(client.Event.SDK_READY_FROM_CACHE, () => {
858+
t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true');
859+
});
862860

863861
await new Promise(res => client.once(client.Event.SDK_READY, res));
864862

865-
t.equal(manager.names().sort().length, 32, 'active splits should be present for evaluation');
863+
t.equal(manager.names().sort().length, 33, 'active splits should be present for evaluation');
866864
t.true(console.log.calledWithMatch('clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'), 'It should log a message about cleaning up cache');
867865

868866
await splitio.destroy();

0 commit comments

Comments
 (0)