# gbdexiedb — Deep Analysis

**File:** `features/gbdexiedb/gbdexie.db.ts` (250 lines)
**Role:** IndexedDB wrapper (Dexie.js v4) — API response cache, tab persistence, login data, org unit lookup
**Consumers:** `gbhttp.service.ts` (primary), 11 other components/services
**Date:** 2026-02-24

---

## Summary

`DexieService` is the project's offline-first cache and persistence layer. It serves four distinct
concerns under one class: API response caching (GET + POST), UI tab persistence, login data storage,
and org-unit lookup data. The caching integration in `gbhttp.service.ts` is the high-traffic path
(every cached API call goes through it). The implementation has several correctness bugs, P0 security
violations, production debug code left in, and performance problems that scale badly.

---

## 1. Database Schema — Version Conflicts (CRITICAL BUG)

```typescript
// lines 58–72 in gbdexie.db.ts
this.version(1).stores({ apiResponses: 'id, endpoint, timestamp' });
this.version(3).stores({ tabs: 'TabId, timestamp' });
this.version(1).stores({ logindto: 'id, timestamp' });  // ← DUPLICATE version(1)
this.version(2).stores({ OrganizationUnitCode: '...' });
```

**Issues:**
- `version(1)` is declared **twice** — Dexie processes the last one. The first declaration
  (`apiResponses`) may be silently overwritten by the second (`logindto`).
- `logindto` table is defined in `version(1).stores()` but **never assigned** (`this.logindto`
  is never initialized — line 76 is commented out). All login methods (`saveLogindata`,
  `getLogindata`, etc.) will throw `TypeError: Cannot read properties of undefined` at runtime.
- Schema versions are non-sequential in code order (1, 3, 1, 2). While Dexie resolves by version
  number not code order, the intent is unreadable and will cause confusion during upgrades.
- `version(3)` for `tabs` skips version 2 — fine if intentional but there is no upgrade logic.

**Fix:** Consolidate into one `version(N)` block with all tables, and enable the `logindto` table
or remove the dead API surface entirely.

```typescript
// Correct pattern
this.version(1).stores({
  apiResponses: 'id, endpoint, timestamp',
  tabs: 'TabId, timestamp',
  logindto: 'id, timestamp',
  OrganizationUnitCode: 'OrganizationUnitCode, CompanyCode, BranchCode, DivisionCode',
});
this.apiResponses = this.table('apiResponses');
this.tabs = this.table('tabs');
this.logindto = this.table('logindto');
this.OrganizationUnitCode = this.table('OrganizationUnitCode');
```

---

## 2. Security Issues

### P0 — `sessionStorage` read in 6 hot-path methods (lines 90–91, 117–118, 172–173, 189–190, 208–209, 217–218)

Every caching method (save/get/remove for GET and POST) reads `LoginDTO` from `sessionStorage`:

```typescript
let LoginDTODetail: any = sessionStorage.getItem('LoginDTO');
let LoginDTO = JSON.parse(LoginDTODetail);
endpoint = endpoint + '/' + LoginDTO.UserCode + '/' + LoginDTO.DatabaseName;
```

**Problems:**
- `sessionStorage` is cleared on tab close — `getItem` returns `null`, `JSON.parse(null)` returns
  `null`, and `LoginDTO.UserCode` throws `TypeError`. This crashes every cache operation silently
  after a tab reload, causing all requests to fall through to the network without caching.
- `sessionStorage` is readable by any script on the page (XSS vector).
- CLAUDE.md mandates: "No tokens or sensitive data in localStorage / sessionStorage — use httpOnly cookies."
- Repeating this pattern 6 times means 6 places to fix and 6 places to break.

**Fix:** Extract a private helper that reads session state once with null-guard:

```typescript
private getUserCacheKey(endpoint: string): string {
  // Prefer injected auth signal over sessionStorage
  const user = this.authStateService.currentUser();
  if (!user) throw new Error('DexieService: no authenticated user for cache key');
  return `${endpoint}/${user.UserCode}/${user.DatabaseName}`;
}
```

Or pass `UserCode`/`DatabaseName` as parameters so callers own the key construction.

### P0 — `console.log` in production (lines 216, 221, 223, 227, 228)

```typescript
console.log("removePostApiResponse called with endpoint:", endpoint);
console.log("List of api's in dexie", entries);          // logs ALL cached data
console.log("api that needed to be deleted", matchingEntries);
console.log(`Deleted all entries for endpoint: ${endpoint}`);
console.log('Remaining entries:', await this.apiResponses.toArray());
```

`entries` is **the entire cache table** — potentially thousands of API responses including
sensitive payloads. This violates CLAUDE.md ("No `console.log` — use `GbConsoleService`") and
is a data exposure risk. Replace with `this.consoleService.log(...)` or remove entirely.

### P1 — No error handling on JSON.parse (lines 91, 118, 173, 190, 209, 218)

`JSON.parse` on a `null` or malformed sessionStorage value will throw synchronously inside an
`async` function and propagate as an unhandled promise rejection. The caller in `gbhttp.service.ts`
has no `try/catch` around cache lookups, so this silently breaks HTTP calls.

---

## 3. Functional Bugs

### BUG — POST cache key does not survive `removePostApiResponse` (lines 175, 222)

`postsaveApiResponse` stores records with `id = hash(endpoint + ':' + JSON.stringify(body))`.
`removePostApiResponse` tries to find them by matching `entry.endpoint === endpoint`.

The stored `endpoint` field is the user-qualified URL
(`/api/foo/UserCode/DatabaseName`). The `removePostApiResponse` call in `gbhttp.service.ts:1051`
passes the raw URL **before** user qualification. So the filter at line 222 will never match —
POST cache entries are never removed by this function.

**Trace:**
```
gbhttp.service.ts:1051  → removePostApiResponse(url)           ← raw url
gbdexie.db.ts:219       → endpoint += UserCode + DatabaseName  ← qualified endpoint
gbdexie.db.ts:222       → filter entries.endpoint === endpoint  ← now qualified
  vs
gbdexie.db.ts:174       → stores endpoint = qualified url      ← qualified in put()
```
This is correct in `save` (qualified before store) and in `remove` (qualified before filter),
so the filter should actually match. **However**, the O(N) full table scan (line 220) is still
a performance bug — see Section 4.

### BUG — `clearAllData` does not clear tabs (line 200)

```typescript
// await this.tabs.clear();   ← intentionally disabled?
await this.apiResponses.clear();
await this.OrganizationUnitCode.clear();
```

This is called on logout. Tabs contain `MenuDetails: IMenuTreeData` — full menu configuration
per tab. After logout, the next user who opens the browser (shared terminal, kiosk) loads the
previous user's tab state and potentially menu permissions. **This is a data leakage bug.**

### BUG — `clearAllData` swallows all errors silently (lines 198–205)

```typescript
} catch (error) {
  // empty — nothing logged, nothing propagated
}
```

If `clearAllData` fails at logout (e.g. IndexedDB quota exceeded), the caller has no way to
know. Callers assume the cache is cleared; it may not be.

### BUG — `gbhttpjsonget` (gbhttp.service.ts:565) passes raw URL without user-qualification

`gbhttpjsonget` calls `dexieService.getApiResponse(url)` and `saveApiResponse(url, response)`
but the URL passed is the raw external URL (e.g. `assets/config.json`). `saveApiResponse` appends
`UserCode + DatabaseName` to it, creating keys like `assets/config.json/admin/GBDB`. Since this
is called for **JSON config files** (not user-specific APIs), the user suffix is wrong and causes
repeated cache misses when `UserCode` changes.

### BUG — `postgetApiResponse` ignores the TTL (gbhttp.service.ts:995)

`gbhttpjsonget` path checks `cached.timestamp < 600000` before using the cache. But
`gbhttp.service.ts:995` uses:

```typescript
return from(this.dexieService.postgetApiResponse(targeturl, criteria)).pipe(
  switchMap((cachedData) => {
    // no timestamp check here — returns stale data indefinitely
    return this.http.post(...)
  })
);
```

There is no `Date.now() - cached.timestamp < 600000` check in this code path. POST cached data
in `gbattachmentpost` is never expired.

### BUG — `logindto` table API is unreachable (lines 123–142)

`saveLogindata`, `getLogindata`, `getAllLogindata`, `deleteLogindata` all reference `this.logindto`
which is never assigned (line 76 is commented out). These will throw at runtime. Either the entire
login caching feature is broken and unused, or this was accidentally disabled.

---

## 4. Performance Issues

### PERF-P1 — Full table scan in `removePostApiResponse` (lines 220–226)

```typescript
const entries = await this.apiResponses.toArray();           // loads ALL records
const matchingEntries = entries.filter(entry => entry.endpoint === endpoint);
for (const entry of matchingEntries) {
  await this.apiResponses.delete(entry.id);                  // N separate deletes
}
```

This loads the entire `apiResponses` table into JavaScript heap, filters in memory, then issues
individual deletes in a loop. At scale (thousands of cached entries), this is:
- O(N) memory allocation
- O(N) JavaScript filtering
- O(M) sequential async IndexedDB round-trips for deletions

**Fix:** Use Dexie's indexed where-clause and bulk delete:

```typescript
// endpoint field is indexed in the schema — use it
await this.apiResponses
  .where('endpoint')
  .equals(endpoint)
  .delete();
```

This executes entirely in the IndexedDB engine with zero JS heap allocation.

### PERF-P2 — LoginDTO re-read from sessionStorage on every cache operation

`sessionStorage.getItem('LoginDTO')` + `JSON.parse()` is called on every `saveApiResponse`,
`getApiResponse`, `postsaveApiResponse`, `postgetApiResponse`, `removeGetApiResponse`,
`removePostApiResponse` — i.e. on every single HTTP request. For a page with 20 API calls, this
is 40 sessionStorage reads + 40 JSON.parse() calls before any cache logic executes.

**Fix:** Cache the parsed LoginDTO in a private signal or field, invalidate on auth events:

```typescript
private cachedUserKey = signal<{ UserCode: string; DatabaseName: string } | null>(null);
```

### PERF-P3 — `console.log('Remaining entries:', await this.apiResponses.toArray())`  (line 228)

Even if `console.log` is removed, this performs a **full table read** on every POST cache removal
just to log the remainder. Remove this line entirely.

### PERF-P4 — No batch operations for tab save

`saveTabdata` is called once per tab open. If restoring 10 tabs on startup, this is 10 sequential
IndexedDB `put()` operations. Use `bulkPut()` for bulk restore scenarios.

### PERF-P5 — No cache size limit or eviction policy

`apiResponses` grows indefinitely. There is no entry count cap, no LRU eviction, no size check.
In a busy app session (hours of use, hundreds of unique API calls), IndexedDB can accumulate
tens of megabytes. Browsers begin throttling IndexedDB at storage quota limits. Consider a
simple count-based or size-based eviction (e.g. keep last 500 entries, remove oldest on overflow).

---

## 5. Maintainability Issues

### MAINT-P1 — Global `String.prototype` mutation (lines 232–249)

```typescript
String.prototype.hashCode = function (): number { ... };
```

Modifying built-in prototypes is a well-known anti-pattern:
- Collides with any other library that defines `hashCode` on `String`
- Shows up in `for...in` loops on string-derived objects in non-strict code
- Cannot be tree-shaken (it is a side effect at module load)
- Impossible to type-safely override per-context

**Fix:** Export a standalone function:

```typescript
export function hashCode(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  return hash;
}
```

### MAINT-P2 — All `any` types (lines 9, 27, 89, 103, 116, 123, 167, 169, 185, 187)

`response: any`, `endpoint: any`, `login: any`, `body: any`, `LoginDetail: any` — all untyped.
This defeats TypeScript's value entirely. Define interfaces:

```typescript
interface LoginSessionData { UserCode: string; DatabaseName: string; }
interface CachedHttpResponse<T = unknown> { Status: number; responseValue: T; /* ... */ }
```

### MAINT-P3 — Single class handles 4 unrelated concerns

`DexieService` manages:
1. HTTP GET cache
2. HTTP POST cache
3. Tab persistence
4. Org unit lookup data
5. Login data storage

These have entirely different access patterns, TTL requirements, and consumers. When a developer
needs to change tab eviction behavior, they must navigate around unrelated caching code.

**Recommendation:** Split into focused services:
- `ApiCacheService` — GET/POST caching with TTL
- `TabPersistenceService` — tab state
- `OrgUnitCacheService` — org unit lookup
- Keep `DexieService` as the base Dexie extension only

### MAINT-P4 — TTL hardcoded in consumers, not in service

The 10-minute TTL (`600000`) is not defined in `DexieService` — it is checked in
`gbhttp.service.ts:510`, `gbhttp.service.ts:566`, and `gbhttp.service.ts:736`. If the TTL needs
to change, three call sites must be updated. Define it as a constant in the service:

```typescript
export const API_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
```

### MAINT-P5 — `Dexievaluestore` field in `TabResponse` (line 15)

The field `Dexievaluestore: string` has an unclear purpose. It is saved but never read in any
visible consumer. Document its intent or remove it.

### MAINT-P6 — `endpoint` parameter typed as `any` in `saveApiResponse` / `getApiResponse`

```typescript
async saveApiResponse(endpoint: any, response: any): Promise<void>
```

`endpoint` should be `string`. Using `any` means callers can pass objects accidentally, causing
`object.hashCode()` to hash `[object Object]` — a silent collision.

---

## 6. Memory / Resource Issues

### MEM-P1 — No cleanup hook on service destroy

`DexieService` is `providedIn: 'root'` (singleton). Dexie holds an open IndexedDB connection
for the app's lifetime — this is expected. However, if the service were ever scoped
(e.g. in a lazy module), the Dexie connection would leak because there is no `ngOnDestroy`
calling `this.close()`.

For robustness, add:
```typescript
ngOnDestroy() { this.close(); }
```

### MEM-P2 — `clearAllData` does not close/reopen connection

After `apiResponses.clear()` the connection remains open with a potentially empty-then-refilled
table. This is fine. But callers that rely on `clearAllData` to free memory (e.g. on logout) get
no confirmation that clearing succeeded (see Section 3 — swallowed errors). If the `try/catch`
swallows a quota error, all subsequent `put()` calls will also fail silently.

---

## 7. Missing Features / Functional Gaps

### GAP-1 — No cache invalidation on logout

`clearAllData` is the intended logout hook but it does not clear `tabs`. A complete logout must:
1. Clear `apiResponses` ✓
2. Clear `OrganizationUnitCode` ✓
3. Clear `tabs` ✗ (currently disabled)
4. Clear `logindto` ✗ (table not initialized)

### GAP-2 — No cache versioning / invalidation on API schema change

If a backend API changes its response shape, old cached data will still be returned to consumers
for up to 10 minutes. There is no mechanism to bust the cache by API version or deployment.

**Recommendation:** Store an app version hash alongside cached responses; invalidate on app
update by comparing `environment.version` to a stored `cacheVersion` key.

### GAP-3 — No offline-first strategy

The service caches responses but there is no mechanism to serve cached data when the network
is unavailable. If `Isstore=false` or the cache is empty, every call falls through to the
network. Consider adding a `fallbackToCache: boolean` flag for offline resilience.

### GAP-4 — No per-endpoint TTL override

All endpoints share the same 10-minute TTL hardcoded in callers. Some data (e.g. org units,
picklist values) could be cached for hours; some data (e.g. dashboard KPIs) should expire in
30 seconds. `saveApiResponse` should accept an optional `ttlMs` parameter.

### GAP-5 — `updateApiResponse` only updates if entry exists; no upsert path

`updateApiResponse` is a no-op if the entry doesn't exist (lines 107–113). If a consumer
calls update before save, the response is silently dropped. Use `put()` instead of conditional
`update()`, or rename to `upsertApiResponse` and align the implementation.

### GAP-6 — Hash collisions are silently destructive

`hashCode()` returns a 32-bit signed integer. The birthday paradox gives ~50% collision
probability at ~65,000 unique cache keys. A collision causes one endpoint's cached data to
**overwrite another endpoint's** data in IndexedDB, returning wrong data to a consumer. No
detection, no warning.

For a cache with potentially hundreds of unique API keys this is low risk today, but consider
using a 64-bit hash or storing the full endpoint string as the primary key (IndexedDB handles
string keys efficiently).

---

## 8. Code Quality / Standards Violations

| Violation | Line(s) | Severity |
|---|---|---|
| `console.log` in production | 216, 221, 223, 227, 228 | P0 |
| `sessionStorage` for sensitive data | 90, 117, 172, 189, 208, 217 | P0 |
| `any` types throughout | 9, 89, 103, 116, 123, 167, 187 | P1 |
| `String.prototype` mutation | 238 | P1 |
| Duplicate `version(1)` declarations | 58, 66 | P1 (correctness) |
| `logindto` table never initialized | 76 | P1 (runtime crash) |
| Empty `catch` block | 203–204 | P1 |
| Hardcoded TTL in consumers | gbhttp.service.ts:510,566,736 | P2 |
| No cache eviction policy | — | P2 |
| Full table scan in removePostApiResponse | 220 | P2 |
| God-class (4 unrelated concerns) | — | P2 |
| Dead API surface (logindata methods) | 123–142 | P3 |
| `Dexievaluestore` unexplained field | 15 | P3 |
| No unit tests | — | P2 |

---

## 9. Recommended Refactoring Priority

1. **Immediate (P0):**
   - Remove all `console.log` statements (lines 216, 221, 223, 227, 228)
   - Add null-guard + error logging around `JSON.parse(sessionStorage.getItem('LoginDTO'))`
   - Fix `clearAllData` to also clear `tabs` and propagate errors

2. **Short-term (P1 correctness):**
   - Fix `version(1)` duplication — consolidate schema into sequential versions
   - Either initialize `this.logindto` or delete all logindata methods and the table
   - Replace `String.prototype.hashCode` with a standalone exported function
   - Replace `any` with typed interfaces

3. **Short-term (performance):**
   - Replace full table scan in `removePostApiResponse` with indexed `.where().delete()`
   - Extract LoginDTO session read into a cached helper (called once per session, not per request)

4. **Medium-term (architecture):**
   - Split into `ApiCacheService` + `TabPersistenceService` + `OrgUnitCacheService`
   - Export `API_CACHE_TTL_MS` constant; move TTL check inside the service
   - Add per-endpoint TTL support via optional parameter
   - Add logout hook that clears all tables including tabs

5. **Long-term:**
   - Add cache eviction (max entry count + LRU or time-based)
   - Add offline fallback mode
   - Add app-version-based cache busting
   - Consider 64-bit hash or string PK to eliminate collision risk
   - Add unit tests covering: cache hit, cache miss, expired entry, null sessionStorage, hash collision

---

## 10. Integration Concerns (gbhttp.service.ts)

- **Constructor injection** (`public dexieService: DexieService` at line 49) — should be
  `private dexieService = inject(DexieService)` per CLAUDE.md.
- `gbhttpjsonget` (line 565) passes raw asset URLs into `saveApiResponse` which appends
  `UserCode + DatabaseName`. JSON config files are not user-specific — this is incorrect and
  wastes cache space per user. Bypass caching for asset URLs or use a separate bucket.
- `gbattachmentpost` (line 995) uses `postgetApiResponse` but **never checks the TTL** on the
  returned cached value. Stale POST responses in this path never expire.
- `console.log('url before', url)` and `console.log('url after', url)` at lines 1047–1050 in
  `gbhttp.service.ts` are also production debug logs that must be removed.
