Skip to content

Commit

Permalink
Use IndexedDB for config caching in web workers + add sample app
Browse files Browse the repository at this point in the history
  • Loading branch information
adams85 committed Nov 12, 2024
1 parent e740e15 commit 15a56df
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ if (value) {
## Sample/Demo apps
- [Plain HTML + JS](https://github.com/configcat/js-unified-sdk/tree/master/samples/html)
- [Plain HTML + JS using ECMAScript module system](https://github.com/configcat/js-unified-sdk/tree/master/samples/html-esm)
- [Bundled HTML + JS running the SDK in a Web Worker](https://github.com/configcat/js-unified-sdk/tree/master/samples/web-worker)
- [Sample Angular web application](https://github.com/configcat/js-unified-sdk/tree/master/samples/angular-sample)
- [Sample React web application](https://github.com/configcat/js-unified-sdk/tree/master/samples/react-sample)
- [Sample React Native application](https://github.com/configcat/js-unified-sdk/tree/master/samples/react-native-sample)
Expand Down
18 changes: 18 additions & 0 deletions samples/web-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "app",
"version": "1.0.0",
"description": "Sample application for ConfigCat JS SDK",
"type": "module",
"private": true,
"scripts": {
"build": "webpack --mode development",
"start": "npm run build & node ../shared/server dist"
},
"dependencies": {
"@configcat/sdk": "^0.0.1"
},
"devDependencies": {
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
}
}
17 changes: 17 additions & 0 deletions samples/web-worker/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function addComponent(value) {
const element = document.createElement("div");

element.innerHTML = "isAwesomeFeatureEnabled: " + value;

document.body.appendChild(element);
}

const myWorker = new Worker("worker.js");

myWorker.onmessage = (e) => {
console.log("Feature flag value received from worker");
addComponent(e.data);
}

myWorker.postMessage("isAwesomeFeatureEnabled");
console.log("Feature flag key posted to worker");
15 changes: 15 additions & 0 deletions samples/web-worker/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as configcat from "@configcat/sdk/browser";

const logger = configcat.createConsoleLogger(configcat.LogLevel.Info); // Setting log level to Info to show detailed feature flag evaluation

// You can instantiate the client with different polling modes. See the Docs: https://configcat.com/docs/sdk-reference/js/#polling-modes
const configCatClient = configcat.getClient("PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ", configcat.PollingMode.AutoPoll, { pollIntervalSeconds: 2, logger: logger });

onmessage = async (e) => {
console.log("Feature flag key received from main script");

const value = await configCatClient.getValueAsync(e.data, false);

postMessage(value);
console.log("Feature flag value posted back to main script");
};
10 changes: 10 additions & 0 deletions samples/web-worker/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": false,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}
25 changes: 25 additions & 0 deletions samples/web-worker/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default {
mode: "production",
entry: {
"main": "./src/index.ts",
"worker": "./src/worker.ts"
},
output: {
filename: "[name].js",
library: { type: "umd" },
},
resolve: {
extensions: [".ts", ".js"]
},
module: {
rules: [
{
test: /.ts$/,
use: [{
loader: "ts-loader"
}]
}
]
},
devtool: "source-map"
};
2 changes: 1 addition & 1 deletion src/browser/LocalStorageCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function getLocalStorage(): Storage | null {
return storage;
}
}
catch (err) { /* intentional no-op */ }
catch { /* intentional no-op */ }

return null;
}
Expand Down
10 changes: 9 additions & 1 deletion src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PollingMode } from "../ConfigCatClientOptions";
import { DefaultEventEmitter } from "../DefaultEventEmitter";
import { getClient as getClientCommon } from "../index.pubternals.core";
import { setupPolyfills } from "../Polyfills";
import { IndexedDBCache } from "../shared/IndexedDBCache";
import CONFIGCAT_SDK_VERSION from "../Version";
import { LocalStorageCache } from "./LocalStorageCache";
import { XmlHttpRequestConfigFetcher } from "./XmlHttpRequestConfigFetcher";
Expand All @@ -12,6 +13,9 @@ import { XmlHttpRequestConfigFetcher } from "./XmlHttpRequestConfigFetcher";

setupPolyfills();

// eslint-disable-next-line @typescript-eslint/naming-convention
declare const WorkerGlobalScope: any;

/**
* Returns an instance of `ConfigCatClient` for the specified SDK Key.
* @remarks This method returns a single, shared instance per each distinct SDK Key.
Expand All @@ -23,8 +27,12 @@ setupPolyfills();
* @param options Options for the specified polling mode.
*/
export function getClient<TMode extends PollingMode | undefined>(sdkKey: string, pollingMode?: TMode, options?: OptionsForPollingMode<TMode>): IConfigCatClient {
const setupCache = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope
? IndexedDBCache.setup
: LocalStorageCache.setup;

return getClientCommon(sdkKey, pollingMode ?? PollingMode.AutoPoll, options,
LocalStorageCache.setup({
setupCache({
configFetcher: new XmlHttpRequestConfigFetcher(),
sdkType: "ConfigCat-UnifiedJS-Browser",
sdkVersion: CONFIGCAT_SDK_VERSION,
Expand Down
10 changes: 9 additions & 1 deletion src/chromium-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { DefaultEventEmitter } from "../DefaultEventEmitter";
import { getClient as getClientCommon } from "../index.pubternals.core";
import { setupPolyfills } from "../Polyfills";
import { FetchApiConfigFetcher } from "../shared/FetchApiConfigFetcher";
import { IndexedDBCache } from "../shared/IndexedDBCache";
import CONFIGCAT_SDK_VERSION from "../Version";
import { ChromeLocalStorageCache } from "./ChromeLocalStorageCache";

/* Package public API for Chromium-based browser extensions */

setupPolyfills();

// eslint-disable-next-line @typescript-eslint/naming-convention
declare const WorkerGlobalScope: any;

/**
* Returns an instance of `ConfigCatClient` for the specified SDK Key.
* @remarks This method returns a single, shared instance per each distinct SDK Key.
Expand All @@ -23,8 +27,12 @@ setupPolyfills();
* @param options Options for the specified polling mode.
*/
export function getClient<TMode extends PollingMode | undefined>(sdkKey: string, pollingMode?: TMode, options?: OptionsForPollingMode<TMode>): IConfigCatClient {
const setupCache = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope
? IndexedDBCache.setup
: ChromeLocalStorageCache.setup;

return getClientCommon(sdkKey, pollingMode ?? PollingMode.AutoPoll, options,
ChromeLocalStorageCache.setup({
setupCache({
configFetcher: new FetchApiConfigFetcher(),
sdkType: "ConfigCat-UnifiedJS-ChromiumExtension",
sdkVersion: CONFIGCAT_SDK_VERSION,
Expand Down
3 changes: 3 additions & 0 deletions src/index.pubternals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export { DefaultEventEmitter } from "./DefaultEventEmitter";

export { LocalStorageCache } from "./browser/LocalStorageCache";

export { IndexedDBCache } from "./shared/IndexedDBCache";

export { XmlHttpRequestConfigFetcher } from "./browser/XmlHttpRequestConfigFetcher";

export { FetchApiConfigFetcher } from "./shared/FetchApiConfigFetcher";

70 changes: 70 additions & 0 deletions src/shared/IndexedDBCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { IConfigCatCache } from "../ConfigCatCache";
import { ExternalConfigCache } from "../ConfigCatCache";
import type { IConfigCatKernel } from "../ConfigCatClient";

const OBJECT_STORE_NAME = "configCache";

type DBConnectionFactory = () => Promise<IDBDatabase>;

export class IndexedDBCache implements IConfigCatCache {
static setup(kernel: IConfigCatKernel, dbConnectionFactoryGetter?: () => DBConnectionFactory | null): IConfigCatKernel {
const dbConnectionFactory = (dbConnectionFactoryGetter ?? getDBConnectionFactory)();
if (dbConnectionFactory) {
kernel.defaultCacheFactory = options => new ExternalConfigCache(new IndexedDBCache(dbConnectionFactory), options.logger);
}
return kernel;
}

constructor(private readonly dbConnectionFactory: DBConnectionFactory) {
}

async set(key: string, value: string): Promise<void> {
const db = await this.dbConnectionFactory();
try {
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(OBJECT_STORE_NAME, "readwrite");
const store = transaction.objectStore(OBJECT_STORE_NAME);
store.put(value, key);
transaction.oncomplete = () => resolve();
transaction.onerror = event => reject((event.target as IDBRequest<IDBValidKey>).error);
});
}
finally { db.close(); }
}

async get(key: string): Promise<string | undefined> {
const db = await this.dbConnectionFactory();
try {
return await new Promise<string | undefined>((resolve, reject) => {
const transaction = db.transaction(OBJECT_STORE_NAME, "readonly");
const store = transaction.objectStore(OBJECT_STORE_NAME);
const storeRequest = store.get(key);
let value: string | undefined;
storeRequest.onsuccess = event => value = (event.target as IDBRequest<any>).result;
transaction.oncomplete = () => resolve(value);
transaction.onerror = event => reject((event.target as IDBRequest<IDBValidKey>).error);
});
}
finally { db.close(); }
}
}

export function getDBConnectionFactory(): DBConnectionFactory | null {
try {
const dbConnectionFactory = () => new Promise<IDBDatabase>((resolve, reject) => {
const openRequest = indexedDB.open("@configcat/sdk");
openRequest.onupgradeneeded = event =>
(event.target as IDBOpenDBRequest).result.createObjectStore(OBJECT_STORE_NAME);
openRequest.onsuccess = event => resolve((event.target as IDBOpenDBRequest).result);
openRequest.onerror = event => reject((event.target as IDBOpenDBRequest).error);
});

// Check if it is possible to connect to the DB.
dbConnectionFactory().then(db => db.close());

return dbConnectionFactory;
}
catch { /* intentional no-op */ }

return null;
}
3 changes: 3 additions & 0 deletions test/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ includeTestModules(testsContext);
testsContext = require.context("../helpers", true, /\.ts$/);
includeTestModules(testsContext);

testsContext = require.context("../shared", false, /IndexedDBCacheTests\.ts$/);
includeTestModules(testsContext);

function includeTestModules(testsContext: Record<string, any>) {
for (const key of testsContext.keys()) {
(testsContext as any)(key);
Expand Down
3 changes: 3 additions & 0 deletions test/chromium-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ includeTestModules(testsContext);
testsContext = require.context("../helpers", true, /\.ts$/);
includeTestModules(testsContext);

testsContext = require.context("../shared", false, /IndexedDBCacheTests\.ts$/);
includeTestModules(testsContext);

function includeTestModules(testsContext: Record<string, any>) {
for (const key of testsContext.keys()) {
(testsContext as any)(key);
Expand Down
20 changes: 20 additions & 0 deletions test/shared/IndexedDBCacheTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { assert } from "chai";
import { IndexedDBCache, getDbConnectionFactory } from "#lib/shared/IndexedDBCache";

describe("IndexedDBCache cache tests", () => {
it("IndexedDBCache works with non latin 1 characters", async function() {
if (typeof indexedDB === "undefined") {
this.skip();
}

const dbConnectionFactory = getDbConnectionFactory();
assert.isNotNull(dbConnectionFactory);

const cache = new IndexedDBCache(dbConnectionFactory!);
const key = "testkey";
const text = "äöüÄÖÜçéèñışğ⢙✓😀";
cache.set(key, text);
const retrievedValue = await cache.get(key);
assert.strictEqual(retrievedValue, text);
});
});

0 comments on commit 15a56df

Please sign in to comment.