|
| 1 | +# TASK-203: Integrate Registry with openDB |
| 2 | + |
| 3 | +## Metadata |
| 4 | + |
| 5 | +- **Task ID**: TASK-203 |
| 6 | +- **Title**: [Registry] Integrate Registry with openDB |
| 7 | +- **Priority**: P0 (Blocker) |
| 8 | +- **Status**: In Progress |
| 9 | +- **Dependencies**: TASK-201 (Registry module available) |
| 10 | +- **Boundary**: `src/main.ts`, `src/release/release-manager.ts` |
| 11 | +- **Estimated**: 3 hours |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 1. Purpose |
| 16 | + |
| 17 | +Integrate the Database Registry (TASK-201) with the `openDB()` function to: |
| 18 | + |
| 19 | +1. Prevent duplicate database opens with lock checking |
| 20 | +2. Register databases after successful open |
| 21 | +3. Unregister databases on close |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## 2. Upstream Dependencies |
| 26 | + |
| 27 | +### Completed Tasks |
| 28 | + |
| 29 | +- **TASK-201**: Database Registry module (`src/registry/database-registry.ts`) |
| 30 | + - `DatabaseRegistry` singleton instance |
| 31 | + - `normalizeDatabaseName()` helper |
| 32 | + - `register()`, `unregister()`, `get()`, `has()`, `list()` methods |
| 33 | + - `checkLock()`, `acquireLock()`, `releaseLock()` lock methods |
| 34 | + - `DatabaseAlreadyOpenError` and `DatabaseNotFoundError` error classes |
| 35 | + |
| 36 | +### Relevant Documentation |
| 37 | + |
| 38 | +- **API Contract**: `agent-docs/05-design/01-contracts/01-api.md` - `openDB()` flow diagram includes registry steps |
| 39 | +- **Core Module**: `agent-docs/05-design/03-modules/core.md` - `openDB()` responsibilities |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## 3. Implementation Specification |
| 44 | + |
| 45 | +### 3.1. Files to Modify |
| 46 | + |
| 47 | +1. **`src/main.ts`** - Add registry integration to `openDB()` |
| 48 | +2. **`src/release/release-manager.ts`** - Add registry integration to `close()` |
| 49 | + |
| 50 | +### 3.2. Changes to `src/main.ts` |
| 51 | + |
| 52 | +**Add import:** |
| 53 | + |
| 54 | +```typescript |
| 55 | +import { DatabaseRegistry } from "./registry/database-registry"; |
| 56 | +``` |
| 57 | + |
| 58 | +**Update `openDB()` function:** |
| 59 | + |
| 60 | +```typescript |
| 61 | +export const openDB = async ( |
| 62 | + filename: string, |
| 63 | + options?: OpenDBOptions, |
| 64 | +): Promise<DBInterface> => { |
| 65 | + abilityCheck(); |
| 66 | + |
| 67 | + // NEW: Check lock before opening |
| 68 | + DatabaseRegistry.checkLock(filename); |
| 69 | + |
| 70 | + const { sendMsg } = createWorkerBridge(); |
| 71 | + const runMutex = createMutex(); |
| 72 | + |
| 73 | + const db = await openReleaseDB({ |
| 74 | + filename, |
| 75 | + options, |
| 76 | + sendMsg, |
| 77 | + runMutex, |
| 78 | + }); |
| 79 | + |
| 80 | + // NEW: Register after successful open |
| 81 | + DatabaseRegistry.register(filename, db); |
| 82 | + |
| 83 | + return db; |
| 84 | +}; |
| 85 | +``` |
| 86 | + |
| 87 | +### 3.3. Changes to `src/release/release-manager.ts` |
| 88 | + |
| 89 | +**Add import:** |
| 90 | + |
| 91 | +```typescript |
| 92 | +import { DatabaseRegistry } from "../registry/database-registry"; |
| 93 | +``` |
| 94 | + |
| 95 | +**Update `close()` function:** |
| 96 | + |
| 97 | +```typescript |
| 98 | +const close = async (): Promise<void> => { |
| 99 | + return runMutex(async () => { |
| 100 | + await sendMsg(SqliteEvent.CLOSE); |
| 101 | + // NEW: Unregister from registry after close |
| 102 | + // Need access to filename - will require passing filename to openReleaseDB |
| 103 | + }); |
| 104 | +}; |
| 105 | +``` |
| 106 | + |
| 107 | +**Note**: The `close()` function needs access to `normalizedFilename` to call `unregister()`. This requires: |
| 108 | + |
| 109 | +1. Adding `filename` to the `ReleaseManagerDeps` type |
| 110 | +2. Passing `filename` from `main.ts` to `openReleaseDB()` |
| 111 | + |
| 112 | +### 3.4. Type Updates |
| 113 | + |
| 114 | +**Update `src/release/types.ts`:** |
| 115 | + |
| 116 | +```typescript |
| 117 | +export type ReleaseManagerDeps = { |
| 118 | + filename: string; // NEW: Add filename for unregister on close |
| 119 | + options?: OpenDBOptions; |
| 120 | + sendMsg: <TRes, TReq = unknown>( |
| 121 | + event: SqliteEvent, |
| 122 | + payload?: TReq, |
| 123 | + ) => Promise<TRes>; |
| 124 | + runMutex: <T>(callback: () => Promise<T>) => Promise<T>; |
| 125 | +}; |
| 126 | +``` |
| 127 | + |
| 128 | +**Update `src/main.ts` call:** |
| 129 | + |
| 130 | +```typescript |
| 131 | +const db = await openReleaseDB({ |
| 132 | + filename, // NEW: Pass filename |
| 133 | + options, |
| 134 | + sendMsg, |
| 135 | + runMutex, |
| 136 | +}); |
| 137 | +``` |
| 138 | + |
| 139 | +**Update `src/release/release-manager.ts` function signature:** |
| 140 | + |
| 141 | +```typescript |
| 142 | +export const openReleaseDB = async ({ |
| 143 | + filename, // NEW: Add filename parameter |
| 144 | + options, |
| 145 | + sendMsg, |
| 146 | + runMutex, |
| 147 | +}: ReleaseManagerDeps): Promise<DBInterface> => { |
| 148 | + // ... existing code ... |
| 149 | + const normalizedFilename = normalizeFilename(filename); |
| 150 | + |
| 151 | + // ... existing code ... |
| 152 | + |
| 153 | + const close = async (): Promise<void> => { |
| 154 | + return runMutex(async () => { |
| 155 | + await sendMsg(SqliteEvent.CLOSE); |
| 156 | + DatabaseRegistry.unregister(normalizedFilename); // NEW: Unregister |
| 157 | + }); |
| 158 | + }; |
| 159 | + // ... |
| 160 | +}; |
| 161 | +``` |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## 4. Definition of Done (DoD) |
| 166 | + |
| 167 | +TASK-203 is COMPLETE when: |
| 168 | + |
| 169 | +1. **Code Changes**: |
| 170 | + - [ ] `src/main.ts` imports `DatabaseRegistry` |
| 171 | + - [ ] `src/main.ts` calls `checkLock()` before `openReleaseDB()` |
| 172 | + - [ ] `src/main.ts` calls `register()` after successful `openReleaseDB()` |
| 173 | + - [ ] `src/release/types.ts` includes `filename` in `ReleaseManagerDeps` |
| 174 | + - [ ] `src/release/release-manager.ts` imports `DatabaseRegistry` |
| 175 | + - [ ] `src/release/release-manager.ts` calls `unregister()` in `close()` |
| 176 | + |
| 177 | +2. **Error Handling**: |
| 178 | + - [ ] Opening same database twice throws `DatabaseAlreadyOpenError` |
| 179 | + - [ ] Error message: "Database '{filename}' is already open" |
| 180 | + |
| 181 | +3. **Testing**: |
| 182 | + - [ ] E2E test: Opening same database twice throws error |
| 183 | + - [ ] E2E test: Database registered after successful open |
| 184 | + - [ ] E2E test: Database unregistered after close |
| 185 | + - [ ] All existing tests still pass |
| 186 | + |
| 187 | +4. **Documentation**: |
| 188 | + - [ ] Status board updated (`agent-docs/00-control/01-status.md`) |
| 189 | + - [ ] Task catalog updated to `[x] TASK-203` |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## 5. Sequence Diagram |
| 194 | + |
| 195 | +```mermaid |
| 196 | +sequenceDiagram |
| 197 | + participant App as Application |
| 198 | + participant openDB as openDB() |
| 199 | + participant Reg as DatabaseRegistry |
| 200 | + participant RM as openReleaseDB() |
| 201 | + participant Worker as Worker |
| 202 | +
|
| 203 | + App->>openDB: openDB("myapp") |
| 204 | + openDB->>openDB: abilityCheck() |
| 205 | +
|
| 206 | + openDB->>Reg: checkLock("myapp") |
| 207 | + alt Already open |
| 208 | + Reg-->>openDB: throw DatabaseAlreadyOpenError |
| 209 | + openDB-->>App: Error |
| 210 | + else Available |
| 211 | + Reg-->>openDB: pass |
| 212 | + openDB->>RM: openReleaseDB({filename, ...}) |
| 213 | + RM->>Worker: OPEN database |
| 214 | + Worker-->>RM: DBInterface created |
| 215 | + RM-->>openDB: DBInterface |
| 216 | + openDB->>Reg: register("myapp", db) |
| 217 | + openDB-->>App: DBInterface |
| 218 | + end |
| 219 | +
|
| 220 | + Note over App: Later... |
| 221 | +
|
| 222 | + App->>RM: db.close() |
| 223 | + RM->>Worker: CLOSE |
| 224 | + Worker-->>RM: closed |
| 225 | + RM->>Reg: unregister("myapp") |
| 226 | + RM-->>App: void |
| 227 | +``` |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## 6. Edge Cases |
| 232 | + |
| 233 | +1. **Normalized Filename**: Registry uses `normalizeDatabaseName()` which adds `.sqlite3` suffix. The `openReleaseDB()` uses `normalizeFilename()`. Both should produce consistent results. |
| 234 | + |
| 235 | +2. **Close Timing**: The unregister should happen AFTER the worker CLOSE is successful, not before. |
| 236 | + |
| 237 | +3. **Error on Open**: If `openReleaseDB()` throws after `checkLock()` passes, the database is never registered. This is correct behavior. |
| 238 | + |
| 239 | +4. **Double Close**: If `close()` is called twice, the second `unregister()` should be safe (it's a no-op if the key doesn't exist). |
| 240 | + |
| 241 | +--- |
| 242 | + |
| 243 | +## 7. Testing Plan |
| 244 | + |
| 245 | +### E2E Test: Registry Integration |
| 246 | + |
| 247 | +**File**: `tests/e2e/registry-integration.e2e.test.ts` (new file) |
| 248 | + |
| 249 | +```typescript |
| 250 | +import { describe, it, expect, beforeEach } from "vitest"; |
| 251 | +import { openDB } from "web-sqlite-js"; |
| 252 | +import { DatabaseRegistry } from "../../src/registry/database-registry"; |
| 253 | + |
| 254 | +describe("Registry Integration (TASK-203)", () => { |
| 255 | + beforeEach(() => { |
| 256 | + DatabaseRegistry._clear(); // Clear registry between tests |
| 257 | + }); |
| 258 | + |
| 259 | + it("should throw error when opening same database twice", async () => { |
| 260 | + const db1 = await openDB("test-db"); |
| 261 | + expect(db1).toBeDefined(); |
| 262 | + |
| 263 | + await expect(openDB("test-db")).rejects.toThrow( |
| 264 | + "Database 'test-db.sqlite3' is already open", |
| 265 | + ); |
| 266 | + |
| 267 | + await db1.close(); |
| 268 | + }); |
| 269 | + |
| 270 | + it("should register database after successful open", async () => { |
| 271 | + const db = await openDB("myapp"); |
| 272 | + expect(DatabaseRegistry.has("myapp")).toBe(true); |
| 273 | + expect(DatabaseRegistry.get("myapp")).toBe(db); |
| 274 | + await db.close(); |
| 275 | + }); |
| 276 | + |
| 277 | + it("should unregister database after close", async () => { |
| 278 | + const db = await openDB("myapp"); |
| 279 | + expect(DatabaseRegistry.has("myapp")).toBe(true); |
| 280 | + |
| 281 | + await db.close(); |
| 282 | + expect(DatabaseRegistry.has("myapp")).toBe(false); |
| 283 | + }); |
| 284 | + |
| 285 | + it("should allow opening same database after close", async () => { |
| 286 | + const db1 = await openDB("test-db"); |
| 287 | + await db1.close(); |
| 288 | + |
| 289 | + // Should not throw - database was closed and unregistered |
| 290 | + const db2 = await openDB("test-db"); |
| 291 | + expect(db2).toBeDefined(); |
| 292 | + await db2.close(); |
| 293 | + }); |
| 294 | +}); |
| 295 | +``` |
| 296 | + |
| 297 | +--- |
| 298 | + |
| 299 | +## 8. References |
| 300 | + |
| 301 | +- **Registry Module**: `src/registry/database-registry.ts` |
| 302 | +- **Task Catalog**: `agent-docs/07-taskManager/02-task-catalog.md#TASK-203` |
| 303 | +- **API Contract**: `agent-docs/05-design/01-contracts/01-api.md#openDB` |
| 304 | +- **Core Module LLD**: `agent-docs/05-design/03-modules/core.md` |
0 commit comments