From cf6e8429ba64dc75c6a6f7ff7c0af9bdea194306 Mon Sep 17 00:00:00 2001 From: CheerC Date: Wed, 24 Jun 2026 13:52:14 +0800 Subject: [PATCH] refactor: extract LockManager to LockManager.js.html IIFE module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #129 Phase 1 (PR2/7): Extract 6 lock management methods. Changes: - Create LockManager.js.html with IIFE pattern (function(App){...})(App) - Move 6 methods from JavaScript.html App object: _getLocks, _saveLocks, acquireLock, releaseLock, releaseCurrentLock, refreshLockHeartbeat - Convert this.xxx → App.xxx in extracted methods (key difference from PR1) - Update Index.html: add LockManager.js include after UtilityFunctions.js - Update appWiringContracts.test.js: move methods to IIFE_EXTRACTED, count 41→37 - Update syncMethodsWiring.test.js: load LockManager source, update patterns Follows spec: 1147 tests pass, ESLint 0 errors, coverage unchanged. Scope gate: 5 files (new .html + JavaScript.html + Index.html + 2 wiring tests). Agend-Agent: cb-team-impl Agend-Branch: refactor/phase1-pr2-lock-manager Agend-Issued-At: 2026-06-24T05:47:40.128365+00:00 --- Index.html | 1 + JavaScript.html | 60 +++------------------ LockManager.js.html | 70 +++++++++++++++++++++++++ tests/unit/appWiringContracts.test.js | 18 +++---- tests/unit/syncMethodsWiring.test.js | 75 +++++++++++++++++++-------- 5 files changed, 140 insertions(+), 84 deletions(-) create mode 100644 LockManager.js.html diff --git a/Index.html b/Index.html index f70241a..52ed464 100644 --- a/Index.html +++ b/Index.html @@ -474,6 +474,7 @@

PDF 下載選項 + diff --git a/JavaScript.html b/JavaScript.html index 450ad63..27fc5de 100644 --- a/JavaScript.html +++ b/JavaScript.html @@ -739,60 +739,12 @@ // --- UTILITY --- - _getLocks: function () { - try { - return JSON.parse(localStorage.getItem('gemini_schedule_locks') || '{}'); - } catch (e) { - return {}; - } - }, - - _saveLocks: function (locks) { - localStorage.setItem('gemini_schedule_locks', JSON.stringify(locks)); - }, - - acquireLock: function (scheduleId) { - const locks = this._getLocks(); - const existingLock = locks[scheduleId]; - const now = Date.now(); - - if (existingLock && existingLock.tabId !== this.tabId) { - const isStale = (now - existingLock.timestamp) > 15000; // 15 second stale threshold - if (!isStale) { - return false; // Lock is held by another tab - } - // If we are here, the lock is stale, so we break it. - } - - // Acquire or update the lock - locks[scheduleId] = { tabId: this.tabId, timestamp: now }; - this._saveLocks(locks); - return true; - }, - - releaseLock: function (scheduleId) { - if (!scheduleId) return; - const locks = this._getLocks(); - if (locks[scheduleId] && locks[scheduleId].tabId === this.tabId) { - delete locks[scheduleId]; - this._saveLocks(locks); - } - }, - - releaseCurrentLock: function () { - this.releaseLock(this.activeScheduleId); - }, - - refreshLockHeartbeat: function () { - if (this.isReadOnly || !this.activeScheduleId || this.activeScheduleId === AppConfig.ALL_SCHEDULES_ID) { - return; - } - const locks = this._getLocks(); - if (locks[this.activeScheduleId] && locks[this.activeScheduleId].tabId === this.tabId) { - locks[this.activeScheduleId].timestamp = Date.now(); - this._saveLocks(locks); - } - }, + // _getLocks — moved to LockManager.js.html (Phase 1 PR2) + // _saveLocks — moved to LockManager.js.html (Phase 1 PR2) + // acquireLock — moved to LockManager.js.html (Phase 1 PR2) + // releaseLock — moved to LockManager.js.html (Phase 1 PR2) + // releaseCurrentLock — moved to LockManager.js.html (Phase 1 PR2) + // refreshLockHeartbeat — moved to LockManager.js.html (Phase 1 PR2) _collectFromScheduleData: function (dataSource, collectorFn) { const resultSet = new Set(); diff --git a/LockManager.js.html b/LockManager.js.html new file mode 100644 index 0000000..79b7cc8 --- /dev/null +++ b/LockManager.js.html @@ -0,0 +1,70 @@ + diff --git a/tests/unit/appWiringContracts.test.js b/tests/unit/appWiringContracts.test.js index db4709d..2a5fd55 100644 --- a/tests/unit/appWiringContracts.test.js +++ b/tests/unit/appWiringContracts.test.js @@ -75,7 +75,7 @@ const EXTRACTED_TO_LIB = new Set([ // appLifecycleHelpers.js (new — this wave) 'loadInitialSchedules', 'loadSchedule', 'canManageCurrentScheduleSettings', 'loadAndApplyPersistedFilters', 'applyFilters', 'clearAdvancedFilters', - 'clearAllFilters', 'refreshLockHeartbeat', + 'clearAllFilters', 'saveSchedulesToLocal', ]); @@ -98,9 +98,6 @@ const NOT_EXTRACTABLE = new Set([ 'loadDataFromServer', // ServerApi + state orchestration (result processing extracted) 'isCurrentUserAdmin', // Global var IS_ADMIN (trivial, 1 line) 'printScheduleToPdf', // jsPDF + DOM + ServerApi (massively coupled) - 'acquireLock', // Already extracted as createLockManager in frontendMocks.js - 'releaseLock', // Already extracted as createLockManager in frontendMocks.js - 'releaseCurrentLock', // Wrapper around releaseLock (1 line) ]); /** @@ -108,8 +105,6 @@ const NOT_EXTRACTABLE = new Set([ * These are tested indirectly through their public callers. */ const PRIVATE_HELPERS = new Set([ - '_getLocks', - '_saveLocks', '_collectFromScheduleData', '_collectFromAllCourses', '_forEachCourse', @@ -125,6 +120,9 @@ const IIFE_EXTRACTED = new Set([ // UtilityFunctions.js.html (PR1) 'getShortUserName', 'generateUniqueId', 'stringToHashCode', 'timeToMinutes', 'formatTime', 'formatTimestampForFilename', 'hexToRgb', + // LockManager.js.html (PR2) + '_getLocks', '_saveLocks', 'acquireLock', 'releaseLock', + 'releaseCurrentLock', 'refreshLockHeartbeat', ]); // ─── Tests ───────────────────────────────────────────────────────────────── @@ -137,8 +135,8 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { it('should have expected total App method count', () => { // All public methods (excluding underscore-prefixed private helpers) const publicMethods = appMethods.filter(m => !m.startsWith('_')); - // 48 original - 7 IIFE-extracted = 41 remaining in JavaScript.html - expect(publicMethods.length).toBe(41); + // 48 original - 7 PR1 - 4 PR2 public = 37 remaining in JavaScript.html + expect(publicMethods.length).toBe(37); }); it('every public App method should be classified (extracted OR not-extractable)', () => { @@ -160,7 +158,9 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { }); it('private helpers should be accounted for', () => { - const unclassifiedPrivate = privateMethods.filter(m => !PRIVATE_HELPERS.has(m)); + const unclassifiedPrivate = privateMethods.filter(m => + !PRIVATE_HELPERS.has(m) && !IIFE_EXTRACTED.has(m) + ); expect( unclassifiedPrivate, `Unclassified private helpers: ${unclassifiedPrivate.join(', ')}` diff --git a/tests/unit/syncMethodsWiring.test.js b/tests/unit/syncMethodsWiring.test.js index fcd5522..cd856ba 100644 --- a/tests/unit/syncMethodsWiring.test.js +++ b/tests/unit/syncMethodsWiring.test.js @@ -19,6 +19,12 @@ const jsHtmlSource = readFileSync( 'utf-8' ); +// LockManager methods moved to separate IIFE module (Phase 1 PR2) +const lockManagerSource = readFileSync( + resolve(import.meta.dirname, '../../LockManager.js.html'), + 'utf-8' +); + // ─── Helpers (shared pattern from Wave 2) ──────────────────────────────── /** @@ -30,7 +36,16 @@ function extractMethodBody(source, methodName) { const declPattern = new RegExp( `${methodName}\\s*:\\s*(?:async\\s+)?function\\s*\\([^)]*\\)\\s*\\{` ); - const match = declPattern.exec(source); + let match = declPattern.exec(source); + + // Also try IIFE-extracted pattern: App.methodName = function(...) { + if (!match) { + const iifePattern = new RegExp( + `App\\.${methodName}\\s*=\\s*function\\s*\\([^)]*\\)\\s*\\{` + ); + match = iifePattern.exec(source); + } + if (!match) return null; const startIdx = match.index + match[0].length; @@ -117,15 +132,15 @@ const SYNC_METHOD_WIRING = [ 'gemini_schedule_locks', ]], ['releaseCurrentLock', [ - 'this\\.releaseLock', - 'this\\.activeScheduleId', + 'App\\.releaseLock', + 'App\\.activeScheduleId', ]], ['refreshLockHeartbeat', [ - 'this\\.isReadOnly', - 'this\\.activeScheduleId', + 'App\\.isReadOnly', + 'App\\.activeScheduleId', 'AppConfig\\.ALL_SCHEDULES_ID', - 'this\\._getLocks', - 'this\\._saveLocks', + 'App\\._getLocks', + 'App\\._saveLocks', 'Date\\.now', ]], ]; @@ -133,15 +148,15 @@ const SYNC_METHOD_WIRING = [ // Also verify these helper methods that support the lock system const LOCK_HELPER_METHODS = [ ['acquireLock', [ - 'this\\._getLocks', - 'this\\._saveLocks', - 'this\\.tabId', + 'App\\._getLocks', + 'App\\._saveLocks', + 'App\\.tabId', 'Date\\.now', ]], ['releaseLock', [ - 'this\\._getLocks', - 'this\\._saveLocks', - 'this\\.tabId', + 'App\\._getLocks', + 'App\\._saveLocks', + 'App\\.tabId', ]], ]; @@ -153,14 +168,31 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { // ─── 1. Method existence ────────────────────────────────────────────── - describe('all sync methods exist in JavaScript.html', () => { - it.each(ALL_METHODS)( - '%s is declared as a method', + describe('all sync methods exist in JavaScript.html or IIFE modules', () => { + // Non-lock methods from JavaScript.html + const jsHtmlMethods = SYNC_METHOD_WIRING.filter(([n]) => + !['_getLocks', '_saveLocks', 'releaseCurrentLock', 'refreshLockHeartbeat'].includes(n) + ); + it.each(jsHtmlMethods)( + '%s is declared in JavaScript.html', (methodName, _patterns) => { const body = extractMethodBody(jsHtmlSource, methodName); expect(body).not.toBeNull(); } ); + + // Lock methods from LockManager.js.html + const lockMethods = [...ALL_METHODS.filter(([n]) => + ['_getLocks', '_saveLocks', 'acquireLock', 'releaseLock', + 'releaseCurrentLock', 'refreshLockHeartbeat'].includes(n) + )]; + it.each(lockMethods)( + '%s is declared in LockManager.js.html', + (methodName, _patterns) => { + const body = extractMethodBody(lockManagerSource, methodName); + expect(body).not.toBeNull(); + } + ); }); // ─── 2. Wiring correctness ──────────────────────────────────────────── @@ -231,7 +263,8 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { const entry = ALL_METHODS.find(([n]) => n === methodName); if (!entry) continue; const [, patterns] = entry; - const body = extractMethodBody(jsHtmlSource, methodName); + // Lock methods now in LockManager.js.html + const body = extractMethodBody(lockManagerSource, methodName); describe(methodName, () => { it.each(patterns)( @@ -264,16 +297,16 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { }); it('releaseCurrentLock delegates to releaseLock (thin wrapper)', () => { - const body = extractMethodBody(jsHtmlSource, 'releaseCurrentLock'); + const body = extractMethodBody(lockManagerSource, 'releaseCurrentLock'); expect(body).not.toBeNull(); const lines = body.split('\n').filter(l => l.trim().length > 0); expect(lines.length).toBeLessThanOrEqual(5); - expect(containsCall(body, 'this\\.releaseLock\\(this\\.activeScheduleId\\)')).toBe(true); + expect(containsCall(body, 'App\\.releaseLock\\(App\\.activeScheduleId\\)')).toBe(true); }); it('_getLocks and _saveLocks use the same localStorage key', () => { - const getBody = extractMethodBody(jsHtmlSource, '_getLocks'); - const saveBody = extractMethodBody(jsHtmlSource, '_saveLocks'); + const getBody = extractMethodBody(lockManagerSource, '_getLocks'); + const saveBody = extractMethodBody(lockManagerSource, '_saveLocks'); expect(getBody).not.toBeNull(); expect(saveBody).not.toBeNull();