Skip to content

Commit

Permalink
Minor improvements (#47)
Browse files Browse the repository at this point in the history
* Improve base64 decode perf. + add tests + fix code formatting

* Use LocalStorageCache only when localStorage is available + don't swallow exceptions in LocalStorageCache.get/set

* Send etag as query param (ccetag) when SDK runs in browser

* Exclude non-source files so they don't pollute autocompletion/intellisense

* Update to configcat-common v9.1.0

* Bump version
  • Loading branch information
adams85 authored Jan 9, 2024
1 parent 854dda2 commit 9c29bc4
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 45 deletions.
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "configcat-react",
"version": "4.1.0",
"version": "4.2.0",
"scripts": {
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p tsconfig.build.esm.json && gulp esm",
Expand Down Expand Up @@ -33,7 +33,7 @@
"homepage": "https://configcat.com",
"license": "MIT",
"dependencies": {
"configcat-common": "^9.0.0",
"configcat-common": "^9.1.0",
"tslib": "^2.4.1"
},
"peerDependencies": {
Expand Down
32 changes: 29 additions & 3 deletions src/Cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import { LocalStorageCache } from "./Cache";
import { LocalStorageCache, fromUtf8Base64, getLocalStorage, toUtf8Base64 } from "../src/Cache";

it("LocalStorageCache works with non latin 1 characters", () => {
const cache = new LocalStorageCache();
describe("Base64 encode/decode test", () => {
let allBmpChars = "";
for (let i = 0; i <= 0xFFFF; i++) {
if (i < 0xD800 || 0xDFFF < i) { // skip lone surrogate chars
allBmpChars += String.fromCharCode(i);
}
}

for (const input of [
"",
"\n",
"äöüÄÖÜçéèñışğ⢙✓😀",
allBmpChars
]) {
it(`Base64 encode/decode works - input: ${input.slice(0, Math.min(input.length, 128))}`, () => {
expect(fromUtf8Base64(toUtf8Base64(input))).toStrictEqual(input);
});
}
});

describe("LocalStorageCache cache tests", () => {
it("LocalStorageCache works with non latin 1 characters", () => {
const localStorage = getLocalStorage();
expect(localStorage).not.toBeNull();

const cache = new LocalStorageCache(localStorage!);
const key = "testkey";
const text = "äöüÄÖÜçéèñışğ⢙✓😀";
cache.set(key, text);
const retrievedValue = cache.get(key);
expect(retrievedValue).toStrictEqual(text);
expect(window.localStorage.getItem(key)).toStrictEqual("w6TDtsO8w4TDlsOcw6fDqcOow7HEscWfxJ/DosKi4oSi4pyT8J+YgA==");
});
});
70 changes: 46 additions & 24 deletions src/Cache.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,59 @@
import type { IConfigCatCache } from "configcat-common";
import type { IConfigCatCache, IConfigCatKernel } from "configcat-common";
import { ExternalConfigCache } from "configcat-common";

export class LocalStorageCache implements IConfigCatCache {
set(key: string, value: string): void {
try {
localStorage.setItem(key, this.b64EncodeUnicode(value));
}
catch (ex) {
// local storage is unavailable
static setup(kernel: IConfigCatKernel, localStorageGetter?: () => Storage | null): IConfigCatKernel {
const localStorage = (localStorageGetter ?? getLocalStorage)();
if (localStorage) {
kernel.defaultCacheFactory = options => new ExternalConfigCache(new LocalStorageCache(localStorage), options.logger);
}
return kernel;
}

constructor(private readonly storage: Storage) {
}

set(key: string, value: string): void {
this.storage.setItem(key, toUtf8Base64(value));
}

get(key: string): string | undefined {
try {
const configString = localStorage.getItem(key);
if (configString) {
return this.b64DecodeUnicode(configString);
}
}
catch (ex) {
// local storage is unavailable or invalid cache value in localstorage
const configString = this.storage.getItem(key);
if (configString) {
return fromUtf8Base64(configString);
}
return void 0;
}
}

private b64EncodeUnicode(str: string): string {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (_, p1) {
return String.fromCharCode(parseInt(p1, 16))
}));
}
export function getLocalStorage(): Storage | null {
const testKey = "__configcat_localStorage_test";

try {
const storage = window.localStorage;
storage.setItem(testKey, testKey);

let retrievedItem: string | null;
try { retrievedItem = storage.getItem(testKey); }
finally { storage.removeItem(testKey); }

private b64DecodeUnicode(str: string): string {
return decodeURIComponent(Array.prototype.map.call(atob(str), function (c: string) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''));
if (retrievedItem === testKey) {
return storage;
}
}
catch (err) { /* intentional no-op */ }

return null;
}

export function toUtf8Base64(str: string): string {
str = encodeURIComponent(str);
str = str.replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)));
return btoa(str);
}

export function fromUtf8Base64(str: string): string {
str = atob(str);
str = str.replace(/[%\x80-\xFF]/g, m => "%" + m.charCodeAt(0).toString(16));
return decodeURIComponent(str);
}
7 changes: 3 additions & 4 deletions src/ConfigCatProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IConfig, IConfigCatClient, IConfigCatKernel } from "configcat-common";
import { ExternalConfigCache, PollingMode } from "configcat-common";
import { PollingMode } from "configcat-common";
import * as configcatcommon from "configcat-common";
import type { PropsWithChildren } from "react";
import React, { Component } from "react";
Expand Down Expand Up @@ -56,12 +56,11 @@ class ConfigCatProvider extends Component<
private initializeConfigCatClient() {
const { pollingMode, options } = this.props;
const { sdkKey } = this.props;
const configCatKernel: IConfigCatKernel = {
const configCatKernel: IConfigCatKernel = LocalStorageCache.setup({
configFetcher: new HttpConfigFetcher(),
sdkType: "ConfigCat-React",
sdkVersion: CONFIGCAT_SDK_VERSION,
defaultCacheFactory: options => new ExternalConfigCache(new LocalStorageCache(), options.logger)
};
});

initializedClients.set(sdkKey, (initializedClients.get(sdkKey) ?? 0) + 1);
return configcatcommon.getClient(sdkKey, pollingMode ?? PollingMode.AutoPoll, options, configCatKernel);
Expand Down
10 changes: 8 additions & 2 deletions src/ConfigFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class HttpConfigFetcher implements IConfigFetcher {
}
}

fetchLogic(options: OptionsBase, _: string | null): Promise<IFetchResponse> {
fetchLogic(options: OptionsBase, lastEtag: string | null): Promise<IFetchResponse> {
return new Promise<IFetchResponse>((resolve, reject) => {
try {
options.logger.debug("HttpConfigFetcher.fetchLogic() called.");
Expand All @@ -35,10 +35,16 @@ export class HttpConfigFetcher implements IConfigFetcher {
httpRequest.onabort = () => reject(new FetchError("abort"));
httpRequest.onerror = () => reject(new FetchError("failure"));

httpRequest.open("GET", options.getUrl(), true);
let url = options.getUrl();
if (lastEtag) {
// We are sending the etag as a query parameter so if the browser doesn't automatically adds the If-None-Match header, we can transform this query param to the header in our CDN provider.
url += "&ccetag=" + encodeURIComponent(lastEtag);
}
httpRequest.open("GET", url, true);
httpRequest.timeout = options.requestTimeoutMs;
// NOTE: It's intentional that we don't specify the If-None-Match header.
// The browser automatically handles it, adding it manually would cause an unnecessary CORS OPTIONS request.
// In case the browser doesn't handle it, we are transforming the ccetag query parameter to the If-None-Match header in our CDN provider.
httpRequest.send(null);
}
catch (err) {
Expand Down
7 changes: 6 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true
}
},
"exclude": [
"**/node_modules/",
"lib/",
"samples/"
]
}

0 comments on commit 9c29bc4

Please sign in to comment.