From cd47f1ef8ecc22c489bbf8cc53f9d6a044461a9b Mon Sep 17 00:00:00 2001 From: Reversean Date: Mon, 16 Feb 2026 22:28:52 +0300 Subject: [PATCH] refactor(core): HawkStorage abstraction added --- packages/core/src/index.ts | 1 + packages/core/src/storages/hawk-storage.ts | 26 ++++ packages/javascript/package.json | 3 + .../src/storages/hawk-local-storage.ts | 21 ++++ .../tests/storages/hawk-local-storage.test.ts | 57 +++++++++ packages/javascript/tsconfig.test.json | 3 +- yarn.lock | 112 +++++++++++++++++- 7 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/storages/hawk-storage.ts create mode 100644 packages/javascript/src/storages/hawk-local-storage.ts create mode 100644 packages/javascript/tests/storages/hawk-local-storage.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e69de29..6a701eb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -0,0 +1 @@ +export type { HawkStorage } from './storages/hawk-storage'; diff --git a/packages/core/src/storages/hawk-storage.ts b/packages/core/src/storages/hawk-storage.ts new file mode 100644 index 0000000..63c4b41 --- /dev/null +++ b/packages/core/src/storages/hawk-storage.ts @@ -0,0 +1,26 @@ +/** + * Abstract key–value storage contract used by Hawk internals to persist data across sessions. + */ +export interface HawkStorage { + /** + * Returns the value associated with the given key, or `null` if none exists. + * + * @param key - Storage key to look up. + */ + getItem(key: string): string | null; + + /** + * Persists a value under the given key. + * + * @param key - Storage key. + * @param value - Value to store. + */ + setItem(key: string, value: string): void; + + /** + * Removes the entry for the given key. + * + * @param key - Storage key to remove. + */ + removeItem(key: string): void; +} diff --git a/packages/javascript/package.json b/packages/javascript/package.json index 22e9827..75db15a 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -21,6 +21,7 @@ "build": "vite build", "stats": "size-limit > stats.txt", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", "lint": "eslint --fix \"src/**/*.{js,ts}\"" }, @@ -39,10 +40,12 @@ }, "homepage": "https://github.com/codex-team/hawk.javascript#readme", "dependencies": { + "@hawk.so/core": "workspace:^", "error-stack-parser": "^2.1.4" }, "devDependencies": { "@hawk.so/types": "0.5.8", + "@vitest/coverage-v8": "^4.0.18", "jsdom": "^28.0.0", "vite": "^7.3.1", "vite-plugin-dts": "^4.2.4", diff --git a/packages/javascript/src/storages/hawk-local-storage.ts b/packages/javascript/src/storages/hawk-local-storage.ts new file mode 100644 index 0000000..79fb2f1 --- /dev/null +++ b/packages/javascript/src/storages/hawk-local-storage.ts @@ -0,0 +1,21 @@ +import type { HawkStorage } from '@hawk.so/core'; + +/** + * {@link HawkStorage} implementation backed by the browser's {@linkcode localStorage}. + */ +export class HawkLocalStorage implements HawkStorage { + /** @inheritDoc */ + public getItem(key: string): string | null { + return localStorage.getItem(key); + } + + /** @inheritDoc */ + public setItem(key: string, value: string): void { + localStorage.setItem(key, value); + } + + /** @inheritDoc */ + public removeItem(key: string): void { + localStorage.removeItem(key); + } +} diff --git a/packages/javascript/tests/storages/hawk-local-storage.test.ts b/packages/javascript/tests/storages/hawk-local-storage.test.ts new file mode 100644 index 0000000..fa68537 --- /dev/null +++ b/packages/javascript/tests/storages/hawk-local-storage.test.ts @@ -0,0 +1,57 @@ +import type { Mock } from 'vitest'; +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import { HawkLocalStorage } from '../../src/storages/hawk-local-storage'; + +describe('HawkLocalStorage', () => { + let getItemSpy: Mock<(key: string) => string | null>; + let setItemSpy: Mock<(key: string, value: string) => void>; + let removeItemSpy: Mock<(key: string) => void>; + let storage: HawkLocalStorage; + + beforeEach(() => { + localStorage.clear(); + storage = new HawkLocalStorage(); + getItemSpy = vi.spyOn(Storage.prototype, 'getItem'); + setItemSpy = vi.spyOn(Storage.prototype, 'setItem'); + removeItemSpy = vi.spyOn(Storage.prototype, 'removeItem'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return null when key does not exist', () => { + expect(storage.getItem('foo')).toBeNull(); + expect(getItemSpy).toHaveBeenCalledOnce(); + }); + + it('should return value when key exists in storage', () => { + localStorage.setItem('foo', 'bar'); + + expect(storage.getItem('foo')).toEqual('bar'); + expect(getItemSpy).toHaveBeenCalledWith('foo'); + }); + + it('should persist item via setItem()', () => { + storage.setItem('foo', 'bar'); + + expect(setItemSpy).toHaveBeenCalledWith('foo', 'bar'); + expect(localStorage.getItem('foo')).toEqual('bar'); + }); + + it('should remove item via removeItem()', () => { + localStorage.setItem('foo', 'bar'); + storage.removeItem('foo'); + + expect(removeItemSpy).toHaveBeenCalledWith('foo'); + expect(localStorage.getItem('foo')).toBeNull(); + }); + + it('should not affect other keys when removing', () => { + localStorage.setItem('foo', 'bar'); + storage.removeItem('baz'); + + expect(removeItemSpy).toHaveBeenCalledWith('baz'); + expect(localStorage.getItem('foo')).toEqual('bar'); + }); +}); diff --git a/packages/javascript/tsconfig.test.json b/packages/javascript/tsconfig.test.json index 166c447..38db19a 100644 --- a/packages/javascript/tsconfig.test.json +++ b/packages/javascript/tsconfig.test.json @@ -2,7 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": null, - "declaration": false + "declaration": false, + "types": ["vitest/globals"] }, "include": [ "src/**/*", diff --git a/yarn.lock b/yarn.lock index b4ec9e5..a2aacdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -80,7 +80,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.28.5": +"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.29.0": version: 7.29.0 resolution: "@babel/parser@npm:7.29.0" dependencies: @@ -101,6 +101,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: 10c0/1eb1dc93cc17fb7abdcef21a6e7b867d6aa99a7ec88ec8207402b23d9083ab22a8011213f04b2cf26d535f1d22dc26139b7929e6c2134c254bd1e14ba5e678c3 + languageName: node + linkType: hard + "@csstools/color-helpers@npm:^6.0.1": version: 6.0.1 resolution: "@csstools/color-helpers@npm:6.0.1" @@ -576,7 +583,7 @@ __metadata: languageName: node linkType: hard -"@hawk.so/core@workspace:packages/core": +"@hawk.so/core@workspace:^, @hawk.so/core@workspace:packages/core": version: 0.0.0-use.local resolution: "@hawk.so/core@workspace:packages/core" dependencies: @@ -589,7 +596,9 @@ __metadata: version: 0.0.0-use.local resolution: "@hawk.so/javascript@workspace:packages/javascript" dependencies: + "@hawk.so/core": "workspace:^" "@hawk.so/types": "npm:0.5.8" + "@vitest/coverage-v8": "npm:^4.0.18" error-stack-parser: "npm:^2.1.4" jsdom: "npm:^28.0.0" vite: "npm:^7.3.1" @@ -708,7 +717,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24": +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.31": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -1385,6 +1394,30 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:^4.0.18": + version: 4.0.18 + resolution: "@vitest/coverage-v8@npm:4.0.18" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/utils": "npm:4.0.18" + ast-v8-to-istanbul: "npm:^0.3.10" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.1" + obug: "npm:^2.1.1" + std-env: "npm:^3.10.0" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + "@vitest/browser": 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d + languageName: node + linkType: hard + "@vitest/expect@npm:4.0.18": version: 4.0.18 resolution: "@vitest/expect@npm:4.0.18" @@ -1846,6 +1879,17 @@ __metadata: languageName: node linkType: hard +"ast-v8-to-istanbul@npm:^0.3.10": + version: 0.3.11 + resolution: "ast-v8-to-istanbul@npm:0.3.11" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10c0/0667dcb5f42bd16f5d50b8687f3471f9b9d000ea7f8808c3cd0ddabc1ef7d5b1a61e19f498d5ca7b1285e6c185e11d0ae724c4f9291491b50b6340110ce63108 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -3602,6 +3646,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" @@ -3997,6 +4048,34 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.2.0": + version: 3.2.0 + resolution: "istanbul-reports@npm:3.2.0" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/d596317cfd9c22e1394f22a8d8ba0303d2074fe2e971887b32d870e4b33f8464b10f8ccbe6847808f7db485f084eba09e6c2ed706b3a978e4b52f07085b8f9bc + languageName: node + linkType: hard + "jiti@npm:^2.4.2": version: 2.6.1 resolution: "jiti@npm:2.6.1" @@ -4013,6 +4092,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64 + languageName: node + linkType: hard + "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -4249,6 +4335,26 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.5.1": + version: 0.5.2 + resolution: "magicast@npm:0.5.2" + dependencies: + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + source-map-js: "npm:^1.2.1" + checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + "make-fetch-happen@npm:^15.0.0": version: 15.0.3 resolution: "make-fetch-happen@npm:15.0.3"