Skip to content

Commit caf4d87

Browse files
committed
refactor(core): UserManager abstraction added
1 parent 39d9d88 commit caf4d87

16 files changed

Lines changed: 340 additions & 41 deletions

.github/workflows/main.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ jobs:
2222
CI_JOB_NUMBER: 2
2323
steps:
2424
- uses: actions/checkout@v1
25+
with:
26+
fetch-depth: 0
2527
- name: Use Node.js from .nvmrc
2628
uses: actions/setup-node@v6
2729
with:
2830
node-version-file: '.nvmrc'
2931
- run: corepack enable
3032
- run: yarn install
31-
- run: yarn workspace @hawk.so/javascript test
33+
- run: yarn test:modified origin/${{ github.event.pull_request.base.ref }}
3234

3335
build:
3436
runs-on: ubuntu-latest

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"dev": "yarn workspace @hawk.so/javascript dev",
1515
"build:all": "yarn workspaces foreach -Apt run build",
1616
"build:modified": "yarn workspaces foreach --since=\"$@\" -Rpt run build",
17+
"test:all": "yarn workspaces foreach -Apt run test",
18+
"test:modified": "yarn workspaces foreach --since=\"$@\" -Rpt run test",
1719
"stats": "yarn workspace @hawk.so/javascript stats",
1820
"lint": "eslint -c ./.eslintrc.cjs packages/*/src --ext .ts,.js --fix",
1921
"lint-test": "eslint -c ./.eslintrc.cjs packages/*/src --ext .ts,.js"

packages/core/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
}
1818
},
1919
"scripts": {
20-
"build": "vite build"
20+
"build": "vite build",
21+
"test": "vitest run",
22+
"test:coverage": "vitest run --coverage"
2123
},
2224
"repository": {
2325
"type": "git",
@@ -34,7 +36,9 @@
3436
},
3537
"homepage": "https://github.com/codex-team/hawk.javascript#readme",
3638
"devDependencies": {
39+
"@vitest/coverage-v8": "^4.0.18",
3740
"vite": "^7.3.1",
38-
"vite-plugin-dts": "^4.2.4"
41+
"vite-plugin-dts": "^4.2.4",
42+
"vitest": "^4.0.18"
3943
}
4044
}

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type { HawkStorage } from './types/storage';
2+
export type { UserManager } from './types/user-manager';
3+
export { StorageUserManager } from './types/storage-user-manager';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { AffectedUser } from "@hawk.so/types";
2+
import { HawkStorage } from "./storage";
3+
import { UserManager } from "./user-manager";
4+
5+
/**
6+
* Storage key used to persist the user identifier.
7+
*/
8+
const HAWK_USER_STORAGE_KEY = 'hawk-user-id';
9+
10+
/**
11+
* {@link UserManager} implementation that persists the affected user
12+
* via an injected {@link HawkStorage} backend.
13+
*/
14+
export class StorageUserManager implements UserManager {
15+
16+
/**
17+
* Underlying storage used to read and write the user identifier.
18+
*/
19+
private readonly storage: HawkStorage;
20+
21+
/**
22+
* @param storage - Storage backend to use for persistence.
23+
*/
24+
constructor(storage: HawkStorage) {
25+
this.storage = storage;
26+
}
27+
28+
getUser(): AffectedUser | null {
29+
const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY);
30+
if (storedId) {
31+
return {
32+
id: storedId,
33+
};
34+
}
35+
36+
return null;
37+
}
38+
39+
setUser(user: AffectedUser): void {
40+
this.storage.setItem(HAWK_USER_STORAGE_KEY, user.id);
41+
}
42+
43+
clear(): void {
44+
this.storage.removeItem(HAWK_USER_STORAGE_KEY)
45+
}
46+
}

packages/core/src/types/storage.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Abstract key–value storage contract used by Hawk internals
3+
* (e.g. {@link StorageUserManager}) to persist data across sessions.
4+
*/
5+
export interface HawkStorage {
6+
/**
7+
* Returns the value associated with the given key, or `null` if none exists.
8+
*
9+
* @param key - Storage key to look up.
10+
*/
11+
getItem(key: string): string | null
12+
13+
/**
14+
* Persists a value under the given key.
15+
*
16+
* @param key - Storage key.
17+
* @param value - Value to store.
18+
*/
19+
setItem(key: string, value: string): void
20+
21+
/**
22+
* Removes the entry for the given key.
23+
*
24+
* @param key - Storage key to remove.
25+
*/
26+
removeItem(key: string): void
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { AffectedUser } from "@hawk.so/types";
2+
3+
/**
4+
* Contract for user identity managers.
5+
*
6+
* Implementations are responsible for persisting and retrieving the
7+
* {@link AffectedUser} that is attached to every error report sent by the catcher.
8+
*/
9+
export interface UserManager {
10+
/**
11+
* Returns the current affected user, or `null` if none has been set.
12+
*/
13+
getUser(): AffectedUser | null
14+
15+
/**
16+
* Replaces the stored user with the provided one.
17+
*
18+
* @param user - The affected user to persist.
19+
*/
20+
setUser(user: AffectedUser): void
21+
22+
/**
23+
* Removes any previously stored user data.
24+
*/
25+
clear(): void
26+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
import { StorageUserManager } from '../src';
3+
import type { HawkStorage } from '../src';
4+
5+
describe('StorageUserManager', () => {
6+
let storage: HawkStorage;
7+
let manager: StorageUserManager;
8+
9+
beforeEach(() => {
10+
storage = {
11+
getItem: vi.fn().mockReturnValue(null),
12+
setItem: vi.fn(),
13+
removeItem: vi.fn(),
14+
};
15+
manager = new StorageUserManager(storage);
16+
});
17+
18+
it('should return null when storage is empty', () => {
19+
expect(manager.getUser()).toBeNull();
20+
expect(storage.getItem).toHaveBeenCalledWith('hawk-user-id');
21+
});
22+
23+
it('should return user when ID exists in storage', () => {
24+
vi.mocked(storage.getItem).mockReturnValue('test-user-123');
25+
26+
expect(manager.getUser()).toEqual({id: 'test-user-123'});
27+
expect(storage.getItem).toHaveBeenCalledWith('hawk-user-id');
28+
});
29+
30+
it('should persist user ID via setUser()', () => {
31+
manager.setUser({id: 'user-abc'});
32+
33+
expect(storage.setItem).toHaveBeenCalledWith('hawk-user-id', 'user-abc');
34+
});
35+
36+
it('should remove user ID via clear()', () => {
37+
manager.clear();
38+
39+
expect(storage.removeItem).toHaveBeenCalledWith('hawk-user-id');
40+
});
41+
});

packages/core/tsconfig.test.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": null,
5+
"declaration": false,
6+
"types": ["vitest/globals"]
7+
},
8+
"include": [
9+
"src/**/*",
10+
"tests/**/*",
11+
"vitest.config.ts"
12+
]
13+
}

packages/core/vitest.config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
globals: true,
6+
include: ['tests/**/*.test.ts'],
7+
typecheck: {
8+
tsconfig: './tsconfig.test.json',
9+
},
10+
coverage: {
11+
provider: 'v8',
12+
include: ['src/**/*.ts'],
13+
},
14+
},
15+
});

0 commit comments

Comments
 (0)