From e4a84b7c239121acd480c3395e8aabbbf9958d40 Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Thu, 19 Jan 2023 10:01:26 +0530 Subject: [PATCH] chore: support localstorage APIs using appsmith store functions (#19789) ## Description Most JS libraries are written with the intent to run of browsers main thread and not a web worker. JS libraries relies on the support for local storage APIs fail because web worker do not have access to local storage APIs. Examples of libraries that rely on localStorage include Mixpanel-browser, Supabasev2 etc. - Mocks localStorage API by using the respective store operation under the hood. - Autocomplete will not suggest localStorage APIs to promote the use of appsmith store functions. Fixes #19792 ### Type of change - Chore (housekeeping or task changes that don't impact user perception) ### How Has This Been Tested? - Manual 1. Instal supobase - https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.4.0/dist/umd/supabase.min.js and verify that its working properly 2. Instal recommend libraries and verify that it is getting installed properly > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --- .../Evaluation/__tests__/Actions.test.ts | 1 - .../workers/Evaluation/fns/LocalStorage.ts | 30 ++++++ .../fns/__tests__/LocalStorage.test.ts | 102 ++++++++++++++++++ .../src/workers/Evaluation/fns/storeFns.ts | 6 +- .../Evaluation/handlers/setupEvalEnv.ts | 2 + 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 app/client/src/workers/Evaluation/fns/LocalStorage.ts create mode 100644 app/client/src/workers/Evaluation/fns/__tests__/LocalStorage.test.ts diff --git a/app/client/src/workers/Evaluation/__tests__/Actions.test.ts b/app/client/src/workers/Evaluation/__tests__/Actions.test.ts index 55491be532..454335c5b0 100644 --- a/app/client/src/workers/Evaluation/__tests__/Actions.test.ts +++ b/app/client/src/workers/Evaluation/__tests__/Actions.test.ts @@ -4,7 +4,6 @@ import { createEvaluationContext, EvalContext, } from "workers/Evaluation/evaluate"; -import uniqueId from "lodash/uniqueId"; import { MessageType } from "utils/MessageUtil"; import { addDataTreeToContext, diff --git a/app/client/src/workers/Evaluation/fns/LocalStorage.ts b/app/client/src/workers/Evaluation/fns/LocalStorage.ts new file mode 100644 index 0000000000..d1b4212270 --- /dev/null +++ b/app/client/src/workers/Evaluation/fns/LocalStorage.ts @@ -0,0 +1,30 @@ +import get from "lodash/get"; + +export default function initLocalStorage(ctx: typeof globalThis) { + function getItem(key: string) { + //@ts-expect-error no types + return get(ctx.appsmith.store, key); + } + function setItem(key: string, value: any) { + //@ts-expect-error no types + ctx.storeValue(key, value); + } + function removeItem(key: string) { + //@ts-expect-error no types + ctx.removeValue(key); + } + function clear() { + //@ts-expect-error no types + ctx.clearStore(); + } + const localStorage = { + getItem, + setItem, + removeItem, + clear, + }; + Object.defineProperty(ctx, "localStorage", { + enumerable: false, + value: localStorage, + }); +} diff --git a/app/client/src/workers/Evaluation/fns/__tests__/LocalStorage.test.ts b/app/client/src/workers/Evaluation/fns/__tests__/LocalStorage.test.ts new file mode 100644 index 0000000000..1e01a3ca0f --- /dev/null +++ b/app/client/src/workers/Evaluation/fns/__tests__/LocalStorage.test.ts @@ -0,0 +1,102 @@ +import { addPlatformFunctionsToEvalContext } from "ce/workers/Evaluation/Actions"; +import { ENTITY_TYPE } from "design-system"; +import { PluginType } from "entities/Action"; +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { createEvaluationContext } from "workers/Evaluation/evaluate"; +import initLocalStorage from "../LocalStorage"; + +describe("Tests localStorage implementation in worker", () => { + const dataTree: DataTree = { + action1: { + actionId: "123", + pluginId: "", + data: {}, + config: {}, + datasourceUrl: "", + pluginType: PluginType.API, + dynamicBindingPathList: [], + name: "action1", + bindingPaths: {}, + reactivePaths: {}, + isLoading: false, + run: {}, + clear: {}, + responseMeta: { isExecutionSuccess: false }, + ENTITY_TYPE: ENTITY_TYPE.ACTION, + dependencyMap: {}, + logBlackList: {}, + }, + }; + const workerEventMock = jest.fn(); + self.postMessage = workerEventMock; + self.ALLOW_ASYNC = true; + const evalContext = createEvaluationContext({ + dataTree, + resolvedFunctions: {}, + isTriggerBased: true, + context: {}, + }); + + addPlatformFunctionsToEvalContext(evalContext); + initLocalStorage(evalContext as any); + it("setItem()", () => { + const key = "some"; + const value = "thing"; + jest.useFakeTimers(); + evalContext.localStorage.setItem(key, value); + jest.runAllTimers(); + expect(workerEventMock).lastCalledWith({ + messageType: "DEFAULT", + body: { + data: [ + { + payload: { + key: "some", + value: "thing", + persist: true, + }, + type: "STORE_VALUE", + }, + ], + method: "PROCESS_STORE_UPDATES", + }, + }); + }); + it("getItem()", () => { + expect(evalContext.localStorage.getItem("some")).toBe("thing"); + }); + it("removeItem()", () => { + evalContext.localStorage.removeItem("some"); + jest.runAllTimers(); + expect(workerEventMock).lastCalledWith({ + messageType: "DEFAULT", + body: { + data: [ + { + payload: { + key: "some", + }, + type: "REMOVE_VALUE", + }, + ], + method: "PROCESS_STORE_UPDATES", + }, + }); + }); + it("clear()", () => { + evalContext.localStorage.clear(); + jest.runAllTimers(); + expect(workerEventMock).lastCalledWith({ + messageType: "DEFAULT", + body: { + data: [ + { + payload: null, + type: "CLEAR_STORE", + }, + ], + method: "PROCESS_STORE_UPDATES", + }, + }); + }); +}); diff --git a/app/client/src/workers/Evaluation/fns/storeFns.ts b/app/client/src/workers/Evaluation/fns/storeFns.ts index 52465338c0..05b4b294d7 100644 --- a/app/client/src/workers/Evaluation/fns/storeFns.ts +++ b/app/client/src/workers/Evaluation/fns/storeFns.ts @@ -20,7 +20,7 @@ export function initStoreFns(ctx: typeof globalThis) { persist, }, }; - set(self, ["appsmith", "store", key], value); + set(ctx, ["appsmith", "store", key], value); triggerCollector.collect(requestPayload); return Promise.resolve({}); } @@ -33,14 +33,14 @@ export function initStoreFns(ctx: typeof globalThis) { }, }; //@ts-expect-error no types for store - delete self.appsmith.store[key]; + delete ctx.appsmith.store[key]; triggerCollector.collect(requestPayload); return Promise.resolve({}); } function clearStore() { //@ts-expect-error no types for store - self.appsmith.store = {}; + ctx.appsmith.store = {}; triggerCollector.collect({ type: "CLEAR_STORE", payload: null, diff --git a/app/client/src/workers/Evaluation/handlers/setupEvalEnv.ts b/app/client/src/workers/Evaluation/handlers/setupEvalEnv.ts index 4ed9281e3b..320c5897b3 100644 --- a/app/client/src/workers/Evaluation/handlers/setupEvalEnv.ts +++ b/app/client/src/workers/Evaluation/handlers/setupEvalEnv.ts @@ -6,6 +6,7 @@ import overrideTimeout from "../TimeoutOverride"; import { EvalWorkerSyncRequest } from "../types"; import userLogs from "../UserLog"; import { addPlatformFunctionsToEvalContext } from "@appsmith/workers/Evaluation/Actions"; +import initLocalStorage from "../fns/LocalStorage"; export default function() { const libraries = resetJSLibraries(); @@ -26,6 +27,7 @@ export default function() { interceptAndOverrideHttpRequest(); setupDOM(); addPlatformFunctionsToEvalContext(self); + initLocalStorage(self); return true; }