From 330df8ac5af8445c678f07ea9aafcb4b22cbb484 Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 13:19:16 +0530 Subject: [PATCH 01/17] chore: updated github funding yml --- .github/FUNDING.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 62434d1..c17d659 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,13 @@ # These are supported funding model platforms +custom: ["https://www.paypal.com/paypalme/jairajjangle001/usd", "https://github.com/JairajJangle/OpenCV-Catalogue/blob/master/.github/Jairaj_Jangle_Google_Pay_UPI_QR_Code.jpg"] +liberapay: FutureJJ +ko_fi: futurejj + github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: futurejj tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: FutureJJ issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: ["https://www.paypal.com/paypalme/jairajjangle001/usd"] \ No newline at end of file From 1c5bc9d3ff9b78c3706026fc48dd1857413fe1a1 Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 13:19:47 +0530 Subject: [PATCH 02/17] test: updated jest config to ignore tests from node modules and lib dirs --- .gitignore | 3 +++ package.json | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/.gitignore b/.gitignore index 65aa33c..283c843 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ android/keystores/debug.keystore # generated by bob lib/ + +# Test coverage report +coverage/ \ No newline at end of file diff --git a/package.json b/package.json index a55475d..3f84d3d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,17 @@ "@commitlint/config-conventional" ] }, + "jest": { + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ], + "collectCoverage": true, + "coverageReporters": [ + "json", + "html" + ] + }, "release-it": { "git": { "commitMessage": "chore: release ${version}", From e1337aee2e6cbbdfe6eed4fa57691ec2a8e2d08a Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 18:08:07 +0530 Subject: [PATCH 03/17] ci: added github action ci scripts for automated npm publish --- .circleci/config.yml | 98 ------------------------ .github/actions/setup/action.yml | 78 +++++++++++++++++++ .github/workflows/beta-release.yml | 115 +++++++++++++++++++++++++++++ .github/workflows/ci.yml | 109 +++++++++++++++++++++++++++ 4 files changed, 302 insertions(+), 98 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/actions/setup/action.yml create mode 100644 .github/workflows/beta-release.yml create mode 100644 .github/workflows/ci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 902911a..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,98 +0,0 @@ -version: 2.1 - -executors: - default: - docker: - - image: circleci/node:16 - working_directory: ~/project - -commands: - attach_project: - steps: - - attach_workspace: - at: ~/project - -jobs: - install-dependencies: - executor: default - steps: - - checkout - - attach_project - - restore_cache: - keys: - - dependencies-{{ checksum "package.json" }} - - dependencies- - - restore_cache: - keys: - - dependencies-example-{{ checksum "example/package.json" }} - - dependencies-example- - - run: - name: Install dependencies - command: | - yarn install --cwd example --frozen-lockfile - yarn install --frozen-lockfile - - save_cache: - key: dependencies-{{ checksum "package.json" }} - paths: node_modules - - save_cache: - key: dependencies-example-{{ checksum "example/package.json" }} - paths: example/node_modules - - persist_to_workspace: - root: . - paths: . - - lint: - executor: default - steps: - - attach_project - - run: - name: Lint files - command: | - yarn lint - - typescript: - executor: default - steps: - - attach_project - - run: - name: Typecheck files - command: | - yarn typescript - - unit-tests: - executor: default - steps: - - attach_project - - run: - name: Run unit tests - command: | - yarn test --coverage - - store_artifacts: - path: coverage - destination: coverage - - build-package: - executor: default - steps: - - attach_project - - run: - name: Build package - command: | - yarn prepare - -workflows: - build-and-test: - jobs: - - install-dependencies - - lint: - requires: - - install-dependencies - - typescript: - requires: - - install-dependencies - - unit-tests: - requires: - - install-dependencies - - build-package: - requires: - - install-dependencies diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..d92d29a --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,78 @@ +name: Setup +description: Setup Node.js and install dependencies + +runs: + using: composite + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + # Create a minimal .yarnrc.yml without any plugin references + - name: Create minimal Yarn config + run: | + cat > .yarnrc.yml << EOF + nodeLinker: node-modules + nmHoistingLimits: workspaces + EOF + shell: bash + + # Setup Corepack for proper Yarn version management + - name: Setup Corepack and Yarn + run: | + corepack enable + corepack prepare yarn@3.6.1 --activate + shell: bash + + # Create required directory and install plugins + - name: Setup plugins + run: | + mkdir -p .yarn/plugins + yarn plugin import @yarnpkg/plugin-interactive-tools + yarn plugin import @yarnpkg/plugin-workspace-tools + shell: bash + + # Now update .yarnrc.yml to include the plugins + - name: Update Yarn config with plugins + run: | + cat > .yarnrc.yml << EOF + nodeLinker: node-modules + nmHoistingLimits: workspaces + + plugins: + - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs + spec: "@yarnpkg/plugin-interactive-tools" + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs + spec: "@yarnpkg/plugin-workspace-tools" + EOF + shell: bash + + - name: Restore cache + uses: actions/cache/restore@v4 + id: yarn-cache + with: + path: | + **/node_modules + .yarn/cache + .yarn/unplugged + .yarn/install-state.gz + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} + restore-keys: | + ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn install + shell: bash + + - name: Save cache + uses: actions/cache/save@v4 + if: steps.yarn-cache.outputs.cache-hit != 'true' + with: + path: | + **/node_modules + .yarn/cache + .yarn/unplugged + .yarn/install-state.gz + key: ${{ steps.yarn-cache.outputs.cache-primary-key || format('{0}-yarn-{1}-{2}', runner.os, hashFiles('yarn.lock'), hashFiles('**/package.json', '!node_modules/**')) }} \ No newline at end of file diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml new file mode 100644 index 0000000..5b5243c --- /dev/null +++ b/.github/workflows/beta-release.yml @@ -0,0 +1,115 @@ +name: Beta Release +on: + push: + branches: + - beta + pull_request: + branches: + - beta + merge_group: + types: + - checks_requested + +env: + NODE_OPTIONS: --experimental-vm-modules + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Lint files + run: yarn lint + + - name: Typecheck files + run: yarn typecheck + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run unit tests + run: yarn test:cov + + - name: Update Coverage Badge + # GitHub actions: default branch variable + # https://stackoverflow.com/questions/64781462/github-actions-default-branch-variable + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + uses: we-cli/coverage-badge-action@main + + build-library: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install missing dependencies + run: yarn add -D @ark/schema || echo "Package already installed or not needed" + + - name: Build package + run: yarn prepare + + build-web: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Prepare library + run: yarn prepare + + - name: Build example for Web + run: | + yarn example expo export --platform web + + publish-beta: + needs: [lint, test, build-library, build-web] + runs-on: ubuntu-latest + permissions: + contents: write # To publish a GitHub release + issues: write # To comment on released issues + pull-requests: write # To comment on released pull requests + id-token: write # To enable use of OIDC for npm provenance + if: github.ref == 'refs/heads/beta' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensures all tags are fetched + + - name: Setup + uses: ./.github/actions/setup + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" # Use the latest LTS version of Node.js + registry-url: 'https://registry.npmjs.org/' # Specify npm registry + + - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies + run: npm audit signatures # Check the signatures to verify integrity + + - name: Release Beta + run: npx semantic-release # Run semantic-release to manage versioning and publishing + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub token for authentication + + # Why NODE_AUTH_TOKEN instead of NPM_TOKEN: https://github.com/semantic-release/semantic-release/issues/2313 + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # npm token for publishing package + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb61054 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Lint files + run: yarn lint + + - name: Typecheck files + run: yarn typecheck + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run unit tests + run: yarn test:cov + + - name: Update Coverage Badge + # GitHub actions: default branch variable + # https://stackoverflow.com/questions/64781462/github-actions-default-branch-variable + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + uses: we-cli/coverage-badge-action@main + + build-library: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install missing dependencies + run: yarn add -D @ark/schema || echo "Package already installed or not needed" + + - name: Build package + run: yarn prepare + + build-web: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Prepare library + run: yarn prepare + + - name: Build example for Web + run: | + yarn example expo export --platform web + + publish-npm: + needs: [lint, test, build-library, build-web] + runs-on: ubuntu-latest + permissions: + contents: write # To publish a GitHub release + issues: write # To comment on released issues + pull-requests: write # To comment on released pull requests + id-token: write # To enable use of OIDC for npm provenance + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensures all tags are fetched + + - name: Setup + uses: ./.github/actions/setup + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" # Use the latest LTS version of Node.js + registry-url: 'https://registry.npmjs.org/' # Specify npm registry + + - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies + run: npm audit signatures # Check the signatures to verify integrity + + - name: Release + run: npx semantic-release # Run semantic-release to manage versioning and publishing + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub token for authentication + + # Why NODE_AUTH_TOKEN instead of NPM_TOKEN: https://github.com/semantic-release/semantic-release/issues/2313 + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # npm token for publishing package + \ No newline at end of file From a8106d1876945912e88478b0d9b96a6891fd62a5 Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 19:07:09 +0530 Subject: [PATCH 04/17] feat: added async storage like functions: - getAllKeys - multiRemove - multiMerge - mergeItem - multiSet - getAllItems - multiGet BREAKING CHANGE: Keys are now required to be string types --- src/__tests__/index.test.ts | 288 +++++++++++++++++++++++++++++++++++ src/__tests__/index.test.tsx | 86 ----------- src/index.ts | 218 ++++++++++++++++++++++++++ src/index.tsx | 62 -------- 4 files changed, 506 insertions(+), 148 deletions(-) create mode 100644 src/__tests__/index.test.ts delete mode 100644 src/__tests__/index.test.tsx create mode 100644 src/index.ts delete mode 100644 src/index.tsx diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts new file mode 100644 index 0000000..7da353b --- /dev/null +++ b/src/__tests__/index.test.ts @@ -0,0 +1,288 @@ +import SessionStorage from "../index"; + +describe("API Validation", () => { + test("defines setItem()", () => { + expect(typeof SessionStorage.setItem).toBe("function"); + }); + + test("defines getItem()", () => { + expect(typeof SessionStorage.getItem).toBe("function"); + }); + + test("defines removeItem()", () => { + expect(typeof SessionStorage.removeItem).toBe("function"); + }); + + test("defines clear()", () => { + expect(typeof SessionStorage.clear).toBe("function"); + }); + + test("defines key()", () => { + expect(typeof SessionStorage.key).toBe("function"); + }); + + test("defines length", () => { + expect(typeof SessionStorage.length).toBe("number"); + }); + + // New API validation + test("defines multiGet()", () => { + expect(typeof SessionStorage.multiGet).toBe("function"); + }); + + test("defines getAllItems()", () => { + expect(typeof SessionStorage.getAllItems).toBe("function"); + }); + + test("defines multiSet()", () => { + expect(typeof SessionStorage.multiSet).toBe("function"); + }); + + test("defines mergeItem()", () => { + expect(typeof SessionStorage.mergeItem).toBe("function"); + }); + + test("defines multiMerge()", () => { + expect(typeof SessionStorage.multiMerge).toBe("function"); + }); + + test("defines multiRemove()", () => { + expect(typeof SessionStorage.multiRemove).toBe("function"); + }); + + test("defines getAllKeys()", () => { + expect(typeof SessionStorage.getAllKeys).toBe("function"); + }); +}); + +describe("Basic Operations", function () { + beforeEach(() => { + SessionStorage.clear(); + }); + + it("Store and get item", function () { + const objToStore = { + strings: ["ABCD", "EFGH", "IJKL", "MNOP"], + numbers: [1, 2, 3, 4], + booleans: [true, false, true, false], + objects: { + key_1: "val_1", + key_2: "val_2" + }, + functions: [() => { return 0; }, function fun() { return true; }] + }; + + SessionStorage.setItem("long_obj_key", objToStore); + const val = SessionStorage.getItem("long_obj_key"); + + expect(val).toBe(objToStore); + }); + + it("Remove item", function () { + SessionStorage.setItem("number_key", 0); + SessionStorage.removeItem("number_key"); + + const val = SessionStorage.getItem("number_key"); + + expect(val).toBe(undefined); + }); + + it("Get key by index", function () { + SessionStorage.setItem("test_key", "test_value"); + const key = SessionStorage.key(0); + + expect(key).toBe("test_key"); + }); + + it("Get number of key-value pair", function () { + SessionStorage.setItem("test_key", "test_value"); + const length = SessionStorage.length; + + expect(length).toBe(1); + }); + + it("Clear all", function () { + SessionStorage.setItem("string_key", "value"); + SessionStorage.setItem("number_key", 123); + + SessionStorage.clear(); + + const stringVal = SessionStorage.getItem("string_key"); + const numberVal = SessionStorage.getItem("number_key"); + + expect(stringVal).toBe(undefined); + expect(numberVal).toBe(undefined); + }); + + it("Get number of key-value pair after clear", function () { + SessionStorage.setItem("test_key", "value"); + SessionStorage.clear(); + const length = SessionStorage.length; + + expect(length).toBe(0); + }); +}); + +describe("Enhanced Storage APIs", function () { + beforeEach(() => { + SessionStorage.clear(); + }); + + it("multiGet returns multiple values", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", 2); + SessionStorage.setItem("key3", { foo: "bar" }); + + const values = SessionStorage.multiGet(["key1", "key2", "key3", "nonexistent"]); + + expect(values.key1).toBe("value1"); + expect(values.key2).toBe(2); + expect(values.key3).toEqual({ foo: "bar" }); + expect(values.nonexistent).toBe(undefined); + }); + + it("getAllItems returns all stored items", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", 2); + + const allItems = SessionStorage.getAllItems(); + + expect(Object.keys(allItems).length).toBe(2); + expect(allItems.key1).toBe("value1"); + expect(allItems.key2).toBe(2); + }); + + it("multiSet stores multiple values using array format", function () { + SessionStorage.multiSet([ + ["key1", "value1"], + ["key2", 2] + ]); + + expect(SessionStorage.getItem("key1")).toBe("value1"); + expect(SessionStorage.getItem("key2")).toBe(2); + }); + + it("multiSet stores multiple values using object format", function () { + SessionStorage.multiSet({ + key3: true, + key4: { nested: "object" } + }); + + expect(SessionStorage.getItem("key3")).toBe(true); + expect(SessionStorage.getItem("key4")).toEqual({ nested: "object" }); + }); + + it("mergeItem merges object values", function () { + SessionStorage.setItem("obj", { a: 1, b: 2 }); + + const merged = SessionStorage.mergeItem("obj", { b: 3, c: 4 }); + + expect(merged).toEqual({ a: 1, b: 3, c: 4 }); + expect(SessionStorage.getItem("obj")).toEqual({ a: 1, b: 3, c: 4 }); + }); + + it("mergeItem creates new object if key doesn't exist", function () { + const newObj = SessionStorage.mergeItem("newObj", { x: 10 }); + + expect(newObj).toEqual({ x: 10 }); + expect(SessionStorage.getItem("newObj")).toEqual({ x: 10 }); + }); + + it("mergeItem returns undefined when merging with non-object values", function () { + SessionStorage.setItem("string", "value"); + const result = SessionStorage.mergeItem("string", { prop: "value" }); + + expect(result).toBe(undefined); + expect(SessionStorage.getItem("string")).toBe("value"); // Original value should be unchanged + }); + + it("multiMerge merges multiple objects using array format", function () { + SessionStorage.setItem("obj1", { a: 1 }); + SessionStorage.setItem("obj2", { x: 10 }); + + const results = SessionStorage.multiMerge([ + ["obj1", { b: 2 }], + ["obj2", { y: 20 }], + ["obj3", { new: true }] + ]); + + expect(results.obj1).toEqual({ a: 1, b: 2 }); + expect(results.obj2).toEqual({ x: 10, y: 20 }); + expect(results.obj3).toEqual({ new: true }); + + expect(SessionStorage.getItem("obj1")).toEqual({ a: 1, b: 2 }); + expect(SessionStorage.getItem("obj2")).toEqual({ x: 10, y: 20 }); + expect(SessionStorage.getItem("obj3")).toEqual({ new: true }); + }); + + it("multiMerge merges multiple objects using object format", function () { + SessionStorage.setItem("obj1", { a: 1, b: 2 }); + SessionStorage.setItem("obj2", { x: 10, y: 20 }); + + SessionStorage.multiMerge({ + obj1: { c: 3 }, + obj2: { z: 30 } + }); + + expect(SessionStorage.getItem("obj1")).toEqual({ a: 1, b: 2, c: 3 }); + expect(SessionStorage.getItem("obj2")).toEqual({ x: 10, y: 20, z: 30 }); + }); + + it("multiMerge handles non-mergeable values", function () { + SessionStorage.setItem("obj", { a: 1 }); + SessionStorage.setItem("str", "string value"); + + const results = SessionStorage.multiMerge([ + ["obj", { b: 2 }], + ["str", { prop: "value" }] + ]); + + expect(results.obj).toEqual({ a: 1, b: 2 }); + expect(results.str).toBe(undefined); + expect(SessionStorage.getItem("str")).toBe("string value"); // Unchanged + }); + + it("multiRemove removes multiple keys", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", "value2"); + SessionStorage.setItem("key3", "value3"); + + SessionStorage.multiRemove(["key1", "key3"]); + + expect(SessionStorage.getItem("key1")).toBe(undefined); + expect(SessionStorage.getItem("key2")).toBe("value2"); + expect(SessionStorage.getItem("key3")).toBe(undefined); + }); + + it("getAllKeys returns all keys", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", "value2"); + + const keys = SessionStorage.getAllKeys(); + + expect(keys.length).toBe(2); + expect(keys).toContain("key1"); + expect(keys).toContain("key2"); + }); + + it("handles complex nested objects", function () { + const complexObj = { + level1: { + level2: { + level3: { + data: "nested value", + array: [1, 2, { deep: "item" }] + } + }, + sibling: "sibling value" + } + }; + + SessionStorage.setItem("complex", complexObj); + const retrieved = SessionStorage.getItem("complex"); + + expect(retrieved).toEqual(complexObj); + expect(retrieved.level1.level2.level3.data).toBe("nested value"); + expect(retrieved.level1.level2.level3.array[2].deep).toBe("item"); + }); +}); \ No newline at end of file diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx deleted file mode 100644 index 6bbadee..0000000 --- a/src/__tests__/index.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import SessionStorage from "react-native-session-storage"; - -describe("Validator", () => { - test("defines setItem()", () => { - expect(typeof SessionStorage.setItem).toBe("function"); - }); - - test("defines getItem()", () => { - expect(typeof SessionStorage.getItem).toBe("function"); - }); - - test("defines removeItem()", () => { - expect(typeof SessionStorage.removeItem).toBe("function"); - }); - - test("defines clear()", () => { - expect(typeof SessionStorage.clear).toBe("function"); - }); - - test("defines key()", () => { - expect(typeof SessionStorage.key).toBe("function"); - }); - - test("defines length", () => { - expect(typeof SessionStorage.length).toBe("number"); - }); -}); - -describe('Store', function () { - it('Store and get item', function () { - const objToStore = { - strings: ["ABCD", "EFGH", "IJKL", "MNOP"], - numbers: [1, 2, 3, 4], - booleans: [true, false, true, false], - objects: { - key_1: "val_1", - key_2: "val_2" - }, - functions: [() => { return 0; }, function fun() { return true; }] - }; - - SessionStorage.setItem("long_obj_key", objToStore); - const val = SessionStorage.getItem("long_obj_key"); - - expect(val).toBe(objToStore); - }); - - it('Remove item', function () { - SessionStorage.setItem("number_key", 0); - SessionStorage.removeItem("number_key"); - - const val = SessionStorage.getItem("number_key"); - - expect(val).toBe(undefined); - }); - - it('Get key by index', function () { - const key = SessionStorage.key(0); - - expect(key).toBe("long_obj_key"); - }); - - it('Get number of key-value pair', function () { - const length = SessionStorage.length; - - expect(length).toBe(1); - }); - - it('Clear all', function () { - SessionStorage.setItem("string_key", "value"); - - SessionStorage.clear(); - - const longObjVal = SessionStorage.getItem("long_obj_key"); - const stringVal = SessionStorage.getItem("string_key"); - - expect(longObjVal).toBe(undefined); - expect(stringVal).toBe(undefined); - }); - - it('Get number of key-value pair after clear', function () { - const length = SessionStorage.length; - - expect(length).toBe(0); - }); -}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2e197ad --- /dev/null +++ b/src/index.ts @@ -0,0 +1,218 @@ +export class Storage { + private data: Map; + + constructor() { + this.data = new Map(); + } + + /** + * Get key name by index + * @param n index + * @returns key or undefined if index is out of bounds + */ + public key(n: number): string | undefined { + if (n < 0 || n >= this.data.size) return undefined; + let i = 0; + for (const key of this.data.keys()) { + if (i === n) return key; + i++; + } + return undefined; + } + + /** + * Get value by key + * @param key The key to retrieve + * @returns value if present else `undefined` + */ + public getItem(key: string): T | undefined { + if (typeof key !== 'string') { + throw new TypeError('Key must be a string'); + } + return this.data.get(key); + } + + /** + * Get multiple values by their keys + * @param keys Array of keys + * @returns Object with keys and their corresponding values + */ + public multiGet(keys: string[]): Record { + if (!Array.isArray(keys)) { + throw new TypeError('Keys must be an array'); + } + const result: Record = {}; + + for (const key of keys) { + if (typeof key !== 'string') { + throw new TypeError('Each key must be a string'); + } + result[key] = this.data.get(key); + } + + return result; + } + + /** + * Get all key-value pairs + * @returns Object with all keys and their corresponding values + */ + public getAllItems(): Record { + const result: Record = {}; + + this.data.forEach((value, key) => { + result[key] = value; + }); + + return result; + } + + /** + * Get how many key-value pairs are stored + */ + public get length(): number { + return this.data.size; + } + + /** + * Store key-value pair + * @param key The key to store the value under + * @param value The value to store + */ + public setItem(key: string, value: T) { + if (typeof key !== 'string') { + throw new TypeError('Key must be a string'); + } + this.data.set(key, value); + } + + /** + * Store multiple key-value pairs + * @param keyValuePairs Array of [key, value] pairs or object with key-value pairs + */ + public multiSet(keyValuePairs: [string, T][] | Record) { + if (Array.isArray(keyValuePairs)) { + for (const [key, value] of keyValuePairs) { + if (typeof key !== 'string') { + throw new TypeError('Each key must be a string'); + } + this.data.set(key, value); + } + } else { + for (const key in keyValuePairs) { + if (typeof key !== 'string') { + throw new TypeError('Each key must be a string'); + } + const value = keyValuePairs[key]; + if (value !== undefined) { + this.data.set(key, value); + } + } + } + } + + /** + * Merge an object value with an existing value stored at the given key + * @param key The key to merge the value under + * @param value Object to merge + * @returns The merged object or undefined if the existing value is not mergeable + */ + public mergeItem( + key: string, + value: Record + ): Record | undefined { + if (typeof key !== 'string') { + throw new TypeError('Key must be a string'); + } + const existing = this.data.get(key); + + if (existing && typeof existing === 'object' && !Array.isArray(existing)) { + const merged = { ...existing, ...value }; + this.data.set(key, merged as T); + return merged; + } else if (!existing) { + this.data.set(key, value as T); + return value; + } + + return undefined; // Return undefined if existing value is not mergeable + } + + /** + * Merge multiple objects with their corresponding values + * @param keyValuePairs Array of [key, value] pairs or object with key-value pairs + * @returns Object with keys and their merged values + */ + public multiMerge( + keyValuePairs: [string, Record][] | Record> + ): Record | undefined> { + const results: Record | undefined> = {}; + + if (Array.isArray(keyValuePairs)) { + for (const [key, value] of keyValuePairs) { + if (typeof key !== 'string') { + throw new TypeError('Each key must be a string'); + } + results[key] = this.mergeItem(key, value); + } + } else { + for (const key in keyValuePairs) { + if (typeof key !== 'string') { + throw new TypeError('Each key must be a string'); + } + const value = keyValuePairs[key]; + if (value !== undefined) { + results[key] = this.mergeItem(key, value); + } + } + } + + return results; + } + + /** + * Removes value by key + * @param key The key to remove + */ + public removeItem(key: string) { + if (typeof key !== 'string') { + throw new TypeError('Key must be a string'); + } + this.data.delete(key); + } + + /** + * Remove multiple values by their keys + * @param keys Array of keys to remove + */ + public multiRemove(keys: string[]) { + if (!Array.isArray(keys)) { + throw new TypeError('Keys must be an array'); + } + for (const key of keys) { + if (typeof key !== 'string') { + throw new TypeError('Each key must be a string'); + } + this.data.delete(key); + } + } + + /** + * Clear all key-value pairs + */ + public clear() { + this.data.clear(); + } + + /** + * Get all keys + * @returns Array of all keys + */ + public getAllKeys(): string[] { + return [...this.data.keys()]; + } +} + +const SessionStorage = new Storage(); + +export default SessionStorage; diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 0b04eba..0000000 --- a/src/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Original source: https://stackoverflow.com/a/67368639/7040601 - -export class Storage { - private data: Map; - - constructor() { - this.data = new Map(); - } - - /** - * Get key name by index - * @param n index - * @returns key - */ - public key(n: number): string | undefined { - return [...this.data.keys()][n]; - } - - /** - * Get value by key - * @param key - * @returns value if present else `undefined` - */ - public getItem(key: string) { - return this.data.get(key); - } - - /** - * Get how many keys-value pairs are stored - */ - public get length(): number { - return this.data.size; - } - - /** - * Store key-value pair - * @param key - * @param value - */ - public setItem(key: string, value: any) { - this.data.set(key, value); - } - - /** - * Removes value by key - * @param key - */ - public removeItem(key: string) { - this.data.delete(key); - } - - /** - * Clear all keys - */ - public clear() { - this.data = new Map(); - } -} - -const SessionStorage = new Storage(); - -export default SessionStorage; From f3b8ec7776481fd0d85b425aa95d991b0998bf83 Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 19:08:25 +0530 Subject: [PATCH 05/17] style: fixed numerous eslint issues --- babel.config.js | 2 +- eslint.config.js | 117 ++++++-- package.json | 28 +- scripts/bootstrap.js | 18 +- src/__tests__/index.test.ts | 565 ++++++++++++++++++------------------ src/index.ts | 70 ++--- 6 files changed, 443 insertions(+), 357 deletions(-) diff --git a/babel.config.js b/babel.config.js index f842b77..5ae49c3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: ['module:metro-react-native-babel-preset'], + presets: ["module:metro-react-native-babel-preset"], }; diff --git a/eslint.config.js b/eslint.config.js index 3d938db..c299f94 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,35 +1,118 @@ -const { ESLint } = require("eslint"); - -module.exports = new ESLint({ - baseConfig: { - env: { - browser: true, - es2021: true, - }, - extends: [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react-native/all", +module.exports = [ + { + ignores: [ + "node_modules/**", + "coverage/**", + "lib/**", + "example/.expo/**", + "**/*.d.ts", + "babel.config.js", + "example/babel.config.js", + "eslint.config.js", + "scripts/bootstrap.js", + "package.json" ], - parserOptions: { - ecmaFeatures: { - jsx: true, + }, + // Config for TypeScript files in src/ + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parser: require("@typescript-eslint/parser"), + globals: { + browser: true, + es2021: true, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: "./tsconfig.json", }, + }, + plugins: { + react: require("eslint-plugin-react"), + "react-native": require("eslint-plugin-react-native"), + import: require("eslint-plugin-import"), + prettier: require("eslint-plugin-prettier"), + "@typescript-eslint": require("@typescript-eslint/eslint-plugin"), + }, + settings: { + react: { + version: "detect", + }, + }, + rules: { + ...require("eslint-config-prettier").rules, + "react-native/no-unused-styles": "warn", + "react-native/split-platform-components": "warn", + "react-native/no-inline-styles": "warn", + "react-native/no-color-literals": "warn", + "react-native/no-single-element-style-arrays": "warn", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/explicit-module-boundary-types": "warn", + "@typescript-eslint/no-explicit-any": "warn", // Enforce for src/ + }, + }, + // Config for TypeScript files in example/ + { + files: ["example/**/*.{ts,tsx}"], + languageOptions: { ecmaVersion: "latest", sourceType: "module", + parser: require("@typescript-eslint/parser"), + globals: { + browser: true, + es2021: true, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: "./tsconfig.json", + }, + }, + plugins: { + react: require("eslint-plugin-react"), + "react-native": require("eslint-plugin-react-native"), + import: require("eslint-plugin-import"), + prettier: require("eslint-plugin-prettier"), + "@typescript-eslint": require("@typescript-eslint/eslint-plugin"), }, - plugins: ["react", "react-native", "import"], settings: { react: { version: "detect", }, }, rules: { + ...require("eslint-config-prettier").rules, "react-native/no-unused-styles": "warn", "react-native/split-platform-components": "warn", "react-native/no-inline-styles": "warn", "react-native/no-color-literals": "warn", "react-native/no-single-element-style-arrays": "warn", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/explicit-module-boundary-types": "warn", + "@typescript-eslint/no-explicit-any": "off", // Disable for example/ + }, + }, + // Config for JavaScript files + { + files: ["**/*.js", "!src/**/*.js", "!example/**/*.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + node: true, + }, + }, + plugins: { + prettier: require("eslint-plugin-prettier"), + import: require("eslint-plugin-import"), + }, + rules: { + ...require("eslint-config-prettier").rules, }, }, -}); +]; \ No newline at end of file diff --git a/package.json b/package.json index 3f84d3d..e923332 100644 --- a/package.json +++ b/package.json @@ -55,10 +55,15 @@ "@evilmartians/lefthook": "^1.7.15", "@release-it/conventional-changelog": "^8.0.1", "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", "commitlint": "^19.4.1", "eslint": "^9.6.0", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-native": "^5.0.0", "jest": "^29.7.0", "metro-react-native-babel-preset": "^0.77.0", "prettier": "^3.3.3", @@ -99,27 +104,10 @@ } } }, - "eslintConfig": { - "root": true, - "parser": "@babel/eslint-parser", - "extends": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - } - ] - } - }, "eslintIgnore": [ "node_modules/", + "coverage/", + "example/.expo/*", "lib/" ], "prettier": { diff --git a/scripts/bootstrap.js b/scripts/bootstrap.js index 1729189..db4469d 100644 --- a/scripts/bootstrap.js +++ b/scripts/bootstrap.js @@ -1,17 +1,17 @@ -const os = require('os'); -const path = require('path'); -const child_process = require('child_process'); +const os = require("os"); +const path = require("path"); +const child_process = require("child_process"); -const root = path.resolve(__dirname, '..'); +const root = path.resolve(__dirname, ".."); const args = process.argv.slice(2); const options = { cwd: process.cwd(), env: process.env, - stdio: 'inherit', - encoding: 'utf-8', + stdio: "inherit", + encoding: "utf-8", }; -if (os.type() === 'Windows_NT') { +if (os.type() === "Windows_NT") { options.shell = true; } @@ -20,10 +20,10 @@ let result; if (process.cwd() !== root || args.length) { // We're not in the root of the project, or additional arguments were passed // In this case, forward the command to `yarn` - result = child_process.spawnSync('yarn', args, options); + result = child_process.spawnSync("yarn", args, options); } else { // If `yarn` is run without arguments, perform bootstrap - result = child_process.spawnSync('yarn', ['bootstrap'], options); + result = child_process.spawnSync("yarn", ["bootstrap"], options); } process.exitCode = result.status; diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 7da353b..a521e78 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,288 +1,301 @@ import SessionStorage from "../index"; describe("API Validation", () => { - test("defines setItem()", () => { - expect(typeof SessionStorage.setItem).toBe("function"); - }); - - test("defines getItem()", () => { - expect(typeof SessionStorage.getItem).toBe("function"); - }); - - test("defines removeItem()", () => { - expect(typeof SessionStorage.removeItem).toBe("function"); - }); - - test("defines clear()", () => { - expect(typeof SessionStorage.clear).toBe("function"); - }); - - test("defines key()", () => { - expect(typeof SessionStorage.key).toBe("function"); - }); - - test("defines length", () => { - expect(typeof SessionStorage.length).toBe("number"); - }); - - // New API validation - test("defines multiGet()", () => { - expect(typeof SessionStorage.multiGet).toBe("function"); - }); - - test("defines getAllItems()", () => { - expect(typeof SessionStorage.getAllItems).toBe("function"); - }); - - test("defines multiSet()", () => { - expect(typeof SessionStorage.multiSet).toBe("function"); - }); - - test("defines mergeItem()", () => { - expect(typeof SessionStorage.mergeItem).toBe("function"); - }); - - test("defines multiMerge()", () => { - expect(typeof SessionStorage.multiMerge).toBe("function"); - }); - - test("defines multiRemove()", () => { - expect(typeof SessionStorage.multiRemove).toBe("function"); - }); - - test("defines getAllKeys()", () => { - expect(typeof SessionStorage.getAllKeys).toBe("function"); - }); + test("defines setItem()", () => { + expect(typeof SessionStorage.setItem).toBe("function"); + }); + + test("defines getItem()", () => { + expect(typeof SessionStorage.getItem).toBe("function"); + }); + + test("defines removeItem()", () => { + expect(typeof SessionStorage.removeItem).toBe("function"); + }); + + test("defines clear()", () => { + expect(typeof SessionStorage.clear).toBe("function"); + }); + + test("defines key()", () => { + expect(typeof SessionStorage.key).toBe("function"); + }); + + test("defines length", () => { + expect(typeof SessionStorage.length).toBe("number"); + }); + + // New API validation + test("defines multiGet()", () => { + expect(typeof SessionStorage.multiGet).toBe("function"); + }); + + test("defines getAllItems()", () => { + expect(typeof SessionStorage.getAllItems).toBe("function"); + }); + + test("defines multiSet()", () => { + expect(typeof SessionStorage.multiSet).toBe("function"); + }); + + test("defines mergeItem()", () => { + expect(typeof SessionStorage.mergeItem).toBe("function"); + }); + + test("defines multiMerge()", () => { + expect(typeof SessionStorage.multiMerge).toBe("function"); + }); + + test("defines multiRemove()", () => { + expect(typeof SessionStorage.multiRemove).toBe("function"); + }); + + test("defines getAllKeys()", () => { + expect(typeof SessionStorage.getAllKeys).toBe("function"); + }); }); describe("Basic Operations", function () { - beforeEach(() => { - SessionStorage.clear(); - }); - - it("Store and get item", function () { - const objToStore = { - strings: ["ABCD", "EFGH", "IJKL", "MNOP"], - numbers: [1, 2, 3, 4], - booleans: [true, false, true, false], - objects: { - key_1: "val_1", - key_2: "val_2" - }, - functions: [() => { return 0; }, function fun() { return true; }] - }; - - SessionStorage.setItem("long_obj_key", objToStore); - const val = SessionStorage.getItem("long_obj_key"); - - expect(val).toBe(objToStore); - }); - - it("Remove item", function () { - SessionStorage.setItem("number_key", 0); - SessionStorage.removeItem("number_key"); - - const val = SessionStorage.getItem("number_key"); - - expect(val).toBe(undefined); - }); - - it("Get key by index", function () { - SessionStorage.setItem("test_key", "test_value"); - const key = SessionStorage.key(0); - - expect(key).toBe("test_key"); - }); - - it("Get number of key-value pair", function () { - SessionStorage.setItem("test_key", "test_value"); - const length = SessionStorage.length; - - expect(length).toBe(1); - }); - - it("Clear all", function () { - SessionStorage.setItem("string_key", "value"); - SessionStorage.setItem("number_key", 123); - - SessionStorage.clear(); - - const stringVal = SessionStorage.getItem("string_key"); - const numberVal = SessionStorage.getItem("number_key"); - - expect(stringVal).toBe(undefined); - expect(numberVal).toBe(undefined); - }); - - it("Get number of key-value pair after clear", function () { - SessionStorage.setItem("test_key", "value"); - SessionStorage.clear(); - const length = SessionStorage.length; - - expect(length).toBe(0); - }); + beforeEach(() => { + SessionStorage.clear(); + }); + + it("Store and get item", function () { + const objToStore = { + strings: ["ABCD", "EFGH", "IJKL", "MNOP"], + numbers: [1, 2, 3, 4], + booleans: [true, false, true, false], + objects: { + key_1: "val_1", + key_2: "val_2", + }, + functions: [ + () => { + return 0; + }, + function fun() { + return true; + }, + ], + }; + + SessionStorage.setItem("long_obj_key", objToStore); + const val = SessionStorage.getItem("long_obj_key"); + + expect(val).toBe(objToStore); + }); + + it("Remove item", function () { + SessionStorage.setItem("number_key", 0); + SessionStorage.removeItem("number_key"); + + const val = SessionStorage.getItem("number_key"); + + expect(val).toBe(undefined); + }); + + it("Get key by index", function () { + SessionStorage.setItem("test_key", "test_value"); + const key = SessionStorage.key(0); + + expect(key).toBe("test_key"); + }); + + it("Get number of key-value pair", function () { + SessionStorage.setItem("test_key", "test_value"); + const length = SessionStorage.length; + + expect(length).toBe(1); + }); + + it("Clear all", function () { + SessionStorage.setItem("string_key", "value"); + SessionStorage.setItem("number_key", 123); + + SessionStorage.clear(); + + const stringVal = SessionStorage.getItem("string_key"); + const numberVal = SessionStorage.getItem("number_key"); + + expect(stringVal).toBe(undefined); + expect(numberVal).toBe(undefined); + }); + + it("Get number of key-value pair after clear", function () { + SessionStorage.setItem("test_key", "value"); + SessionStorage.clear(); + const length = SessionStorage.length; + + expect(length).toBe(0); + }); }); describe("Enhanced Storage APIs", function () { - beforeEach(() => { - SessionStorage.clear(); - }); - - it("multiGet returns multiple values", function () { - SessionStorage.setItem("key1", "value1"); - SessionStorage.setItem("key2", 2); - SessionStorage.setItem("key3", { foo: "bar" }); - - const values = SessionStorage.multiGet(["key1", "key2", "key3", "nonexistent"]); - - expect(values.key1).toBe("value1"); - expect(values.key2).toBe(2); - expect(values.key3).toEqual({ foo: "bar" }); - expect(values.nonexistent).toBe(undefined); - }); - - it("getAllItems returns all stored items", function () { - SessionStorage.setItem("key1", "value1"); - SessionStorage.setItem("key2", 2); - - const allItems = SessionStorage.getAllItems(); - - expect(Object.keys(allItems).length).toBe(2); - expect(allItems.key1).toBe("value1"); - expect(allItems.key2).toBe(2); - }); - - it("multiSet stores multiple values using array format", function () { - SessionStorage.multiSet([ - ["key1", "value1"], - ["key2", 2] - ]); - - expect(SessionStorage.getItem("key1")).toBe("value1"); - expect(SessionStorage.getItem("key2")).toBe(2); - }); - - it("multiSet stores multiple values using object format", function () { - SessionStorage.multiSet({ - key3: true, - key4: { nested: "object" } - }); - - expect(SessionStorage.getItem("key3")).toBe(true); - expect(SessionStorage.getItem("key4")).toEqual({ nested: "object" }); - }); - - it("mergeItem merges object values", function () { - SessionStorage.setItem("obj", { a: 1, b: 2 }); - - const merged = SessionStorage.mergeItem("obj", { b: 3, c: 4 }); - - expect(merged).toEqual({ a: 1, b: 3, c: 4 }); - expect(SessionStorage.getItem("obj")).toEqual({ a: 1, b: 3, c: 4 }); - }); - - it("mergeItem creates new object if key doesn't exist", function () { - const newObj = SessionStorage.mergeItem("newObj", { x: 10 }); - - expect(newObj).toEqual({ x: 10 }); - expect(SessionStorage.getItem("newObj")).toEqual({ x: 10 }); - }); - - it("mergeItem returns undefined when merging with non-object values", function () { - SessionStorage.setItem("string", "value"); - const result = SessionStorage.mergeItem("string", { prop: "value" }); + beforeEach(() => { + SessionStorage.clear(); + }); + + it("multiGet returns multiple values", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", 2); + SessionStorage.setItem("key3", { foo: "bar" }); - expect(result).toBe(undefined); - expect(SessionStorage.getItem("string")).toBe("value"); // Original value should be unchanged - }); - - it("multiMerge merges multiple objects using array format", function () { - SessionStorage.setItem("obj1", { a: 1 }); - SessionStorage.setItem("obj2", { x: 10 }); - - const results = SessionStorage.multiMerge([ - ["obj1", { b: 2 }], - ["obj2", { y: 20 }], - ["obj3", { new: true }] - ]); - - expect(results.obj1).toEqual({ a: 1, b: 2 }); - expect(results.obj2).toEqual({ x: 10, y: 20 }); - expect(results.obj3).toEqual({ new: true }); - - expect(SessionStorage.getItem("obj1")).toEqual({ a: 1, b: 2 }); - expect(SessionStorage.getItem("obj2")).toEqual({ x: 10, y: 20 }); - expect(SessionStorage.getItem("obj3")).toEqual({ new: true }); - }); + const values = SessionStorage.multiGet([ + "key1", + "key2", + "key3", + "nonexistent", + ]); + + expect(values.key1).toBe("value1"); + expect(values.key2).toBe(2); + expect(values.key3).toEqual({ foo: "bar" }); + expect(values.nonexistent).toBe(undefined); + }); + + it("getAllItems returns all stored items", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", 2); + + const allItems = SessionStorage.getAllItems(); + + expect(Object.keys(allItems).length).toBe(2); + expect(allItems.key1).toBe("value1"); + expect(allItems.key2).toBe(2); + }); + + it("multiSet stores multiple values using array format", function () { + SessionStorage.multiSet([ + ["key1", "value1"], + ["key2", 2], + ]); - it("multiMerge merges multiple objects using object format", function () { - SessionStorage.setItem("obj1", { a: 1, b: 2 }); - SessionStorage.setItem("obj2", { x: 10, y: 20 }); + expect(SessionStorage.getItem("key1")).toBe("value1"); + expect(SessionStorage.getItem("key2")).toBe(2); + }); - SessionStorage.multiMerge({ - obj1: { c: 3 }, - obj2: { z: 30 } - }); - - expect(SessionStorage.getItem("obj1")).toEqual({ a: 1, b: 2, c: 3 }); - expect(SessionStorage.getItem("obj2")).toEqual({ x: 10, y: 20, z: 30 }); - }); - - it("multiMerge handles non-mergeable values", function () { - SessionStorage.setItem("obj", { a: 1 }); - SessionStorage.setItem("str", "string value"); - - const results = SessionStorage.multiMerge([ - ["obj", { b: 2 }], - ["str", { prop: "value" }] - ]); - - expect(results.obj).toEqual({ a: 1, b: 2 }); - expect(results.str).toBe(undefined); - expect(SessionStorage.getItem("str")).toBe("string value"); // Unchanged - }); - - it("multiRemove removes multiple keys", function () { - SessionStorage.setItem("key1", "value1"); - SessionStorage.setItem("key2", "value2"); - SessionStorage.setItem("key3", "value3"); - - SessionStorage.multiRemove(["key1", "key3"]); - - expect(SessionStorage.getItem("key1")).toBe(undefined); - expect(SessionStorage.getItem("key2")).toBe("value2"); - expect(SessionStorage.getItem("key3")).toBe(undefined); - }); - - it("getAllKeys returns all keys", function () { - SessionStorage.setItem("key1", "value1"); - SessionStorage.setItem("key2", "value2"); - - const keys = SessionStorage.getAllKeys(); - - expect(keys.length).toBe(2); - expect(keys).toContain("key1"); - expect(keys).toContain("key2"); - }); - - it("handles complex nested objects", function () { - const complexObj = { - level1: { - level2: { - level3: { - data: "nested value", - array: [1, 2, { deep: "item" }] - } - }, - sibling: "sibling value" - } - }; - - SessionStorage.setItem("complex", complexObj); - const retrieved = SessionStorage.getItem("complex"); - - expect(retrieved).toEqual(complexObj); - expect(retrieved.level1.level2.level3.data).toBe("nested value"); - expect(retrieved.level1.level2.level3.array[2].deep).toBe("item"); - }); -}); \ No newline at end of file + it("multiSet stores multiple values using object format", function () { + SessionStorage.multiSet({ + key3: true, + key4: { nested: "object" }, + }); + + expect(SessionStorage.getItem("key3")).toBe(true); + expect(SessionStorage.getItem("key4")).toEqual({ nested: "object" }); + }); + + it("mergeItem merges object values", function () { + SessionStorage.setItem("obj", { a: 1, b: 2 }); + + const merged = SessionStorage.mergeItem("obj", { b: 3, c: 4 }); + + expect(merged).toEqual({ a: 1, b: 3, c: 4 }); + expect(SessionStorage.getItem("obj")).toEqual({ a: 1, b: 3, c: 4 }); + }); + + it("mergeItem creates new object if key doesn't exist", function () { + const newObj = SessionStorage.mergeItem("newObj", { x: 10 }); + + expect(newObj).toEqual({ x: 10 }); + expect(SessionStorage.getItem("newObj")).toEqual({ x: 10 }); + }); + + it("mergeItem returns undefined when merging with non-object values", function () { + SessionStorage.setItem("string", "value"); + const result = SessionStorage.mergeItem("string", { prop: "value" }); + + expect(result).toBe(undefined); + expect(SessionStorage.getItem("string")).toBe("value"); // Original value should be unchanged + }); + + it("multiMerge merges multiple objects using array format", function () { + SessionStorage.setItem("obj1", { a: 1 }); + SessionStorage.setItem("obj2", { x: 10 }); + + const results = SessionStorage.multiMerge([ + ["obj1", { b: 2 }], + ["obj2", { y: 20 }], + ["obj3", { new: true }], + ]); + + expect(results.obj1).toEqual({ a: 1, b: 2 }); + expect(results.obj2).toEqual({ x: 10, y: 20 }); + expect(results.obj3).toEqual({ new: true }); + + expect(SessionStorage.getItem("obj1")).toEqual({ a: 1, b: 2 }); + expect(SessionStorage.getItem("obj2")).toEqual({ x: 10, y: 20 }); + expect(SessionStorage.getItem("obj3")).toEqual({ new: true }); + }); + + it("multiMerge merges multiple objects using object format", function () { + SessionStorage.setItem("obj1", { a: 1, b: 2 }); + SessionStorage.setItem("obj2", { x: 10, y: 20 }); + + SessionStorage.multiMerge({ + obj1: { c: 3 }, + obj2: { z: 30 }, + }); + + expect(SessionStorage.getItem("obj1")).toEqual({ a: 1, b: 2, c: 3 }); + expect(SessionStorage.getItem("obj2")).toEqual({ x: 10, y: 20, z: 30 }); + }); + + it("multiMerge handles non-mergeable values", function () { + SessionStorage.setItem("obj", { a: 1 }); + SessionStorage.setItem("str", "string value"); + + const results = SessionStorage.multiMerge([ + ["obj", { b: 2 }], + ["str", { prop: "value" }], + ]); + + expect(results.obj).toEqual({ a: 1, b: 2 }); + expect(results.str).toBe(undefined); + expect(SessionStorage.getItem("str")).toBe("string value"); // Unchanged + }); + + it("multiRemove removes multiple keys", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", "value2"); + SessionStorage.setItem("key3", "value3"); + + SessionStorage.multiRemove(["key1", "key3"]); + + expect(SessionStorage.getItem("key1")).toBe(undefined); + expect(SessionStorage.getItem("key2")).toBe("value2"); + expect(SessionStorage.getItem("key3")).toBe(undefined); + }); + + it("getAllKeys returns all keys", function () { + SessionStorage.setItem("key1", "value1"); + SessionStorage.setItem("key2", "value2"); + + const keys = SessionStorage.getAllKeys(); + + expect(keys.length).toBe(2); + expect(keys).toContain("key1"); + expect(keys).toContain("key2"); + }); + + it("handles complex nested objects", function () { + const complexObj = { + level1: { + level2: { + level3: { + data: "nested value", + array: [1, 2, { deep: "item" }], + }, + }, + sibling: "sibling value", + }, + }; + + SessionStorage.setItem("complex", complexObj); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const retrieved: any = SessionStorage.getItem("complex"); + + expect(retrieved).toEqual(complexObj); + expect(retrieved.level1.level2.level3.data).toBe("nested value"); + expect(retrieved.level1.level2.level3.array[2].deep).toBe("item"); + }); +}); diff --git a/src/index.ts b/src/index.ts index 2e197ad..c28335a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export class Storage { +export class Storage { private data: Map; constructor() { @@ -26,8 +26,8 @@ export class Storage { * @returns value if present else `undefined` */ public getItem(key: string): T | undefined { - if (typeof key !== 'string') { - throw new TypeError('Key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Key must be a string"); } return this.data.get(key); } @@ -39,13 +39,13 @@ export class Storage { */ public multiGet(keys: string[]): Record { if (!Array.isArray(keys)) { - throw new TypeError('Keys must be an array'); + throw new TypeError("Keys must be an array"); } const result: Record = {}; for (const key of keys) { - if (typeof key !== 'string') { - throw new TypeError('Each key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Each key must be a string"); } result[key] = this.data.get(key); } @@ -79,9 +79,9 @@ export class Storage { * @param key The key to store the value under * @param value The value to store */ - public setItem(key: string, value: T) { - if (typeof key !== 'string') { - throw new TypeError('Key must be a string'); + public setItem(key: string, value: T): void { + if (typeof key !== "string") { + throw new TypeError("Key must be a string"); } this.data.set(key, value); } @@ -90,18 +90,18 @@ export class Storage { * Store multiple key-value pairs * @param keyValuePairs Array of [key, value] pairs or object with key-value pairs */ - public multiSet(keyValuePairs: [string, T][] | Record) { + public multiSet(keyValuePairs: [string, T][] | Record): void { if (Array.isArray(keyValuePairs)) { for (const [key, value] of keyValuePairs) { - if (typeof key !== 'string') { - throw new TypeError('Each key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Each key must be a string"); } this.data.set(key, value); } } else { for (const key in keyValuePairs) { - if (typeof key !== 'string') { - throw new TypeError('Each key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Each key must be a string"); } const value = keyValuePairs[key]; if (value !== undefined) { @@ -119,14 +119,14 @@ export class Storage { */ public mergeItem( key: string, - value: Record - ): Record | undefined { - if (typeof key !== 'string') { - throw new TypeError('Key must be a string'); + value: Record + ): Record | undefined { + if (typeof key !== "string") { + throw new TypeError("Key must be a string"); } const existing = this.data.get(key); - if (existing && typeof existing === 'object' && !Array.isArray(existing)) { + if (existing && typeof existing === "object" && !Array.isArray(existing)) { const merged = { ...existing, ...value }; this.data.set(key, merged as T); return merged; @@ -144,21 +144,23 @@ export class Storage { * @returns Object with keys and their merged values */ public multiMerge( - keyValuePairs: [string, Record][] | Record> - ): Record | undefined> { - const results: Record | undefined> = {}; + keyValuePairs: + | [string, Record][] + | Record> + ): Record | undefined> { + const results: Record | undefined> = {}; if (Array.isArray(keyValuePairs)) { for (const [key, value] of keyValuePairs) { - if (typeof key !== 'string') { - throw new TypeError('Each key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Each key must be a string"); } results[key] = this.mergeItem(key, value); } } else { for (const key in keyValuePairs) { - if (typeof key !== 'string') { - throw new TypeError('Each key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Each key must be a string"); } const value = keyValuePairs[key]; if (value !== undefined) { @@ -174,9 +176,9 @@ export class Storage { * Removes value by key * @param key The key to remove */ - public removeItem(key: string) { - if (typeof key !== 'string') { - throw new TypeError('Key must be a string'); + public removeItem(key: string): void { + if (typeof key !== "string") { + throw new TypeError("Key must be a string"); } this.data.delete(key); } @@ -185,13 +187,13 @@ export class Storage { * Remove multiple values by their keys * @param keys Array of keys to remove */ - public multiRemove(keys: string[]) { + public multiRemove(keys: string[]): void { if (!Array.isArray(keys)) { - throw new TypeError('Keys must be an array'); + throw new TypeError("Keys must be an array"); } for (const key of keys) { - if (typeof key !== 'string') { - throw new TypeError('Each key must be a string'); + if (typeof key !== "string") { + throw new TypeError("Each key must be a string"); } this.data.delete(key); } @@ -200,7 +202,7 @@ export class Storage { /** * Clear all key-value pairs */ - public clear() { + public clear(): void { this.data.clear(); } From ee42af31bf9491594b6f981ddddb1949bbebecca Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 19:09:45 +0530 Subject: [PATCH 06/17] style: updated lefthook config to ignore ignored files warning --- lefthook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lefthook.yml b/lefthook.yml index 065a491..a8d5d2f 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,7 +4,7 @@ pre-commit: lint: files: git diff --name-only @{push} glob: "*.{js,ts,jsx,tsx}" - run: npx eslint {files} + run: npx eslint {files} --no-warn-ignored types: files: git diff --name-only @{push} glob: "*.{js,ts, jsx, tsx}" From b7878d9ec6b51b97a37c663d5e9dd71e7477f5a8 Mon Sep 17 00:00:00 2001 From: Jairaj Jangle <25704330+JairajJangle@users.noreply.github.com> Date: Sat, 17 May 2025 19:11:31 +0530 Subject: [PATCH 07/17] feat: renovated the example app to showcase all features --- example/app.json | 17 +- example/app/colors.ts | 11 + example/app/helper.ts | 18 ++ example/app/index.tsx | 555 ++++++++++++++++++++++++++++++++++------ example/app/styles.ts | 113 ++++++++ example/babel.config.js | 2 +- example/package.json | 29 ++- 7 files changed, 655 insertions(+), 90 deletions(-) create mode 100644 example/app/colors.ts create mode 100644 example/app/helper.ts create mode 100644 example/app/styles.ts diff --git a/example/app.json b/example/app.json index dddf149..6cfa310 100644 --- a/example/app.json +++ b/example/app.json @@ -14,13 +14,23 @@ "backgroundColor": "#ffffff" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "infoPlist": { + "UIViewControllerBasedStatusBarAppearance": true + }, + "statusBar": { + "style": "dark-content", + "backgroundColor": "transparent", + "hidden": false, + "translucent": true + } }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "edgeToEdgeEnabled": true }, "web": { "bundler": "metro", @@ -31,7 +41,8 @@ "expo-router" ], "experiments": { - "typedRoutes": true + "typedRoutes": true, + "reactCompiler": true } } } diff --git a/example/app/colors.ts b/example/app/colors.ts new file mode 100644 index 0000000..c820790 --- /dev/null +++ b/example/app/colors.ts @@ -0,0 +1,11 @@ +export const COLORS = { + primary: '#3d527f', + white: '#fff', + coral: '#ff6f61', + green: '#28a745', + red: '#dc3545', + darkGray: '#333', + mediumGray: '#555', + lightGray: '#ddd', + border: '#3d527f', // Reused for borderBottomColor +} as const; \ No newline at end of file diff --git a/example/app/helper.ts b/example/app/helper.ts new file mode 100644 index 0000000..516af2e --- /dev/null +++ b/example/app/helper.ts @@ -0,0 +1,18 @@ +// Helper to normalize quotes in JSON strings (fixes iOS smart quotes) +const normalizeJSONString = (str: string): string => { + // Replace curly quotes with straight quotes + return str + .replace(/[\u2018\u2019]/g, "'") // Replace single curly quotes + .replace(/[\u201C\u201D]/g, '"'); // Replace double curly quotes +}; + +// Helper to safely parse JSON with iOS smart quotes +export const safeJSONParse = (str: string): any => { + try { + // Normalize quotes before parsing + const normalizedStr = normalizeJSONString(str); + return JSON.parse(normalizedStr); + } catch (e: any) { + throw new Error("Invalid JSON format: " + e.message); + } +}; diff --git a/example/app/index.tsx b/example/app/index.tsx index cb4dcd1..05f3dbc 100644 --- a/example/app/index.tsx +++ b/example/app/index.tsx @@ -1,90 +1,499 @@ -import * as React from 'react'; +import * as React from "react"; import { - StyleSheet, View, Text, TextInput, - Button, - SafeAreaView -} from 'react-native'; -import SessionStorage from 'react-native-session-storage'; + TouchableOpacity, + ScrollView, +} from "react-native"; +import SessionStorage from "react-native-session-storage"; +import { useState, useCallback, useEffect } from "react"; +import { LinearGradient } from "expo-linear-gradient"; +import { Ionicons, Entypo } from "@expo/vector-icons"; +import { safeJSONParse } from "./helper"; +import { StatusBar } from "expo-status-bar"; +import { + SafeAreaProvider, + SafeAreaView, + useSafeAreaInsets, +} from "react-native-safe-area-context"; +import styles from "./styles"; -export default function App() { - const [storedVal, updateStoredVal] = React.useState(); - const [inputText, setInputText] = React.useState(""); +interface StorageItem { + key: string; + value: any; +} - const setVal = React.useCallback(() => { - SessionStorage.setItem("my_key", inputText); - }, [inputText]); +export default function App(): React.JSX.Element { + const initialOutput = Object.freeze({ + message: "Results will appear here...", + success: true, + }); - function getVal(): any { - const val = SessionStorage.getItem("my_key"); - updateStoredVal(val); - } + const [inputKey, setInputKey] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [output, setOutput] = useState<{ message: string; success: boolean; }>( + initialOutput + ); + const [allItems, setAllItems] = useState([]); - return ( - - Input Value to Store - + const { top } = useSafeAreaInsets(); + + // Load items on first render + useEffect(() => { + refreshItems(); + }, []); + + // Helper to refresh displayed items + const refreshItems = useCallback(() => { + const items = SessionStorage.getAllItems(); + setAllItems( + Object.entries(items).map(([key, value]) => ({ + key, + value: JSON.stringify(value, null, 2), + })) + ); + }, []); + + // Helper to display results + const displayResult = (message: string, success = true) => { + setOutput({ message, success }); + setTimeout(() => setOutput(initialOutput), 3000); // Clear after 3 seconds + }; + + // Basic Operations + const handleSetItem = useCallback(() => { + try { + if (!inputKey.trim()) { + displayResult("Error: Key cannot be empty", false); + return; + } + + let value: any = inputValue; + try { + // Only parse if it looks like JSON + if ( + inputValue.trim().startsWith("{") || + inputValue.trim().startsWith("[") + ) { + value = safeJSONParse(inputValue); + } + } catch (e) { + console.error(e); + // If parsing fails, use the raw string + } + + SessionStorage.setItem(inputKey, value); + displayResult(`Stored ${inputKey}: ${JSON.stringify(value)}`); + refreshItems(); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, [inputKey, inputValue]); + + const handleGetItem = useCallback(() => { + try { + if (!inputKey.trim()) { + displayResult("Error: Key cannot be empty", false); + return; + } + + const value = SessionStorage.getItem(inputKey); + displayResult(`Retrieved ${inputKey}: ${JSON.stringify(value, null, 2)}`); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, [inputKey]); + + const handleRemoveItem = useCallback(() => { + try { + if (!inputKey.trim()) { + displayResult("Error: Key cannot be empty", false); + return; + } + + SessionStorage.removeItem(inputKey); + displayResult(`Removed ${inputKey}`); + refreshItems(); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, [inputKey]); + + const handleClear = useCallback(() => { + try { + SessionStorage.clear(); + displayResult("Cleared all items"); + refreshItems(); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, []); + + const handleGetKey = useCallback(() => { + try { + // For getKey, we use the inputValue as the index number + const indexStr = inputValue.trim(); + if (!indexStr) { + displayResult("Error: Index cannot be empty", false); + return; + } + + const index = parseInt(indexStr, 10); + if (isNaN(index)) { + displayResult("Error: Index must be a number", false); + return; + } + + const key = SessionStorage.key(index); + displayResult(`Key at index ${index}: ${key || "undefined"}`); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, [inputValue]); + + const handleGetLength = useCallback(() => { + try { + const length = SessionStorage.length; + displayResult(`Storage length: ${length}`); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, []); + + // Enhanced Operations + const handleMultiGet = useCallback(() => { + try { + if (!inputKey.trim()) { + displayResult("Error: Keys cannot be empty", false); + return; + } - + const keys = inputKey.split(",").map((k) => k.trim()); + const values = SessionStorage.multiGet(keys); + displayResult(`MultiGet: ${JSON.stringify(values, null, 2)}`); + } catch (e: any) { + displayResult(`Error: ${e.message}`, false); + } + }, [inputKey]); -