|
1 | 1 | import { HttpAdapter } from '@oierdb/adapter-http'; |
2 | | -import { IDBAdapter } from '@oierdb/adapter-idb'; |
3 | 2 | import { OIerDbClient } from '@oierdb/core'; |
4 | | -import { parseOIerDbData } from '@oierdb/parser'; |
5 | 3 |
|
6 | | -import { backendEndpoint, staticDataVersionUrl } from './constant'; |
7 | | -import { OIerDbClientStatusEnum, setStatus } from './status'; |
8 | | -import { getResultUrl, getStaticUrl } from './util'; |
| 4 | +import { backendEndpoint } from './constant'; |
| 5 | +import { OIerDbClientStatusEnum, setStatus, setupSwStatusListener, SwStatusEnum } from './status'; |
9 | 6 |
|
10 | 7 | /** |
11 | | - * Initialize the OIerDbClient with both HTTP and IndexedDB adapters. |
12 | | - * This function sets up the global OIerDbClientInstance. |
| 8 | + * Initialize the OIerDbClient with HttpAdapter. |
| 9 | + * The endpoint is determined by SW availability: |
| 10 | + * - If SW is ready: use current origin (SW will intercept /api/v1/* requests) |
| 11 | + * - If SW is not ready: use backendEndpoint directly |
13 | 12 | * |
14 | 13 | * Steps: |
15 | | - * - (Before calling this function) Set status to Initializing. |
16 | | - * - Create an instance of HttpAdapter pointing to the remote OIer API. |
17 | | - * - Create an instance of IDBAdapter using the browser's IndexedDB. |
18 | | - * - Check the backend availability. (health check can be done via getVersion API, use error handling to determine unavailability) |
19 | | - * If backend is available: |
20 | | - * - Get the latest version from the backend. |
21 | | - * If the local IndexedDB version is outdated: |
22 | | - * - Set status to InitializedPartially, and use HttpAdapter to create OIerDbClient first. |
23 | | - * - Load data in the background to update IndexedDB. |
24 | | - * - Once data is loaded, switch client's adapter to IDBAdapter and set status to Initialized. |
25 | | - * If the local IndexedDB version is up-to-date: |
26 | | - * - Set status to Initialized and use IDBAdapter directly to create OIerDbClient. |
27 | | - * If backend is not available: |
28 | | - * - Get the latest version from static data source. |
29 | | - * If the local IndexedDB version is outdated: |
30 | | - * - Load data from static source into IndexedDB. |
31 | | - * - Set status to Initialized once done. (No InitializedPartially state here since no backend is available.) |
32 | | - * - Use IDBAdapter to create OIerDbClient. |
33 | | - * If the local IndexedDB version is up-to-date: |
34 | | - * - Set status to Initialized and use IDBAdapter directly to create OIerDbClient. |
35 | | - * If cannot load data from static source: |
36 | | - * - Set status to Uninitialized and throw an error. |
| 14 | + * - Check if SW is registered and ready |
| 15 | + * - Create HttpAdapter with appropriate baseUrl |
| 16 | + * - Set up OIerDbClient with the adapter |
| 17 | + * - Set up Service Worker status listener for status updates |
| 18 | + * - When SW becomes ready, switch to origin-based endpoint |
37 | 19 | */ |
38 | 20 | const initClientAsync = async () => { |
39 | | - const httpAdapter = new HttpAdapter({ |
40 | | - baseUrl: backendEndpoint, |
41 | | - }); |
42 | | - const idbAdapter = new IDBAdapter(indexedDB, IDBKeyRange); |
| 21 | + setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '初始化数据查询模块' }); |
43 | 22 |
|
44 | | - setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '初始化数据库适配器' }); |
| 23 | + // Check if SW is registered and controlling the page |
| 24 | + const swReady = navigator.serviceWorker?.controller != null; |
| 25 | + console.log('[Client] SW ready:', swReady); |
45 | 26 |
|
46 | | - let backendAvailable = false; |
47 | | - let backendVersion = ''; |
| 27 | + // Use backendEndpoint directly if SW is not ready |
| 28 | + // Otherwise use origin so SW can intercept requests |
| 29 | + const baseUrl = swReady ? window.location.origin : backendEndpoint; |
| 30 | + console.log('[Client] Using baseUrl:', baseUrl); |
48 | 31 |
|
49 | | - // Check backend availability |
50 | | - try { |
51 | | - setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '检查后端服务可用性' }); |
52 | | - const versionResponse = await httpAdapter.getVersion(); |
53 | | - backendVersion = versionResponse.data_version; |
54 | | - backendAvailable = true; |
55 | | - } catch (error) { |
56 | | - console.warn('Backend is not available:', error); |
57 | | - backendAvailable = false; |
58 | | - } |
| 32 | + const httpAdapter = new HttpAdapter({ baseUrl }); |
59 | 33 |
|
60 | | - console.log('BackendAvailable:', backendAvailable, 'Version:', backendVersion); |
| 34 | + setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '检查服务可用性' }); |
61 | 35 |
|
62 | | - // Get local IndexedDB version |
63 | | - let localVersion = ''; |
64 | | - let localVersionAvailable = false; |
65 | 36 | try { |
66 | | - const localVersionResponse = await idbAdapter.getVersion(); |
67 | | - localVersion = localVersionResponse.data_version; |
68 | | - localVersionAvailable = await idbAdapter.checkAvailability(localVersion); |
69 | | - } catch (error) { |
70 | | - console.warn('Failed to get local version:', error); |
71 | | - } |
| 37 | + // Test connectivity by getting version |
| 38 | + const version = await httpAdapter.getVersion(); |
| 39 | + console.log('[Client] API version:', version.data_version); |
72 | 40 |
|
73 | | - console.log('Local IndexedDB version:', localVersion); |
74 | | - |
75 | | - if (backendAvailable) { |
76 | | - // Backend is available |
77 | | - if (localVersion && localVersionAvailable && localVersion === backendVersion) { |
78 | | - // Local version is up-to-date, use IDB adapter directly |
79 | | - setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '使用本地数据库' }); |
80 | | - globalThis.OIerDbClientInstance = new OIerDbClient(idbAdapter); |
81 | | - setStatus({ type: OIerDbClientStatusEnum.Initialized, text: '' }); |
82 | | - } else { |
83 | | - // Local version is outdated, use HTTP adapter first |
84 | | - setStatus({ |
85 | | - type: OIerDbClientStatusEnum.InitializedPartially, |
86 | | - text: '使用在线数据服务 [后台: 更新本地数据库]', |
87 | | - }); |
88 | | - globalThis.OIerDbClientInstance = new OIerDbClient(httpAdapter); |
89 | | - |
90 | | - // Load data in background |
91 | | - loadDataInBackground(idbAdapter, backendVersion); |
92 | | - } |
93 | | - } else { |
94 | | - // Backend is not available, use static data source |
95 | | - // Get the latest version from static data source |
96 | | - let staticVersion = ''; |
97 | | - try { |
98 | | - setStatus({ |
99 | | - type: OIerDbClientStatusEnum.Initializing, |
100 | | - text: '检查静态数据版本 [后端: 不可用]', |
101 | | - }); |
102 | | - const versionResponse = await fetch(staticDataVersionUrl); |
103 | | - const versionData = await versionResponse.json(); |
104 | | - staticVersion = versionData.data_version; |
105 | | - } catch (error) { |
106 | | - console.warn('Failed to get static data version:', error); |
107 | | - } |
| 41 | + // Create client with HTTP adapter |
| 42 | + globalThis.OIerDbClientInstance = new OIerDbClient(httpAdapter); |
108 | 43 |
|
109 | | - console.log('Static data version:', staticVersion); |
110 | | - |
111 | | - if (localVersion && localVersionAvailable && localVersion === staticVersion) { |
112 | | - // Local version is up-to-date with static data |
113 | | - setStatus({ |
114 | | - type: OIerDbClientStatusEnum.Initializing, |
115 | | - text: '使用本地数据库', |
116 | | - }); |
117 | | - globalThis.OIerDbClientInstance = new OIerDbClient(idbAdapter); |
118 | | - setStatus({ type: OIerDbClientStatusEnum.Initialized, text: '' }); |
119 | | - } else if (localVersion && localVersionAvailable && !staticVersion) { |
120 | | - // Cannot get static version, but have local data - use it anyway |
121 | | - setStatus({ |
122 | | - type: OIerDbClientStatusEnum.Initializing, |
123 | | - text: '使用本地数据库', |
124 | | - }); |
125 | | - globalThis.OIerDbClientInstance = new OIerDbClient(idbAdapter); |
126 | | - setStatus({ type: OIerDbClientStatusEnum.Initialized, text: '离线模式' }); |
127 | | - } else { |
128 | | - // Local version is outdated or doesn't exist, need to load from static source |
129 | | - try { |
130 | | - setStatus({ |
131 | | - type: OIerDbClientStatusEnum.Initializing, |
132 | | - text: '加载数据', |
133 | | - }); |
134 | | - await loadDataFromStaticSource(idbAdapter, staticVersion); |
135 | | - } catch (error) { |
136 | | - console.error('Failed to load data from static source:', error); |
137 | | - setStatus({ type: OIerDbClientStatusEnum.Uninitialized, text: '加载失败' }); |
138 | | - throw new Error('Cannot initialize OIerDbClient: backend unavailable and no local data'); |
139 | | - } |
| 44 | + // Set up SW status listener to update UI based on SW state |
| 45 | + setupSwStatusListener(); |
| 46 | + |
| 47 | + // If SW wasn't ready, listen for it to become ready and switch endpoint |
| 48 | + if (!swReady) { |
| 49 | + waitForSwAndSwitchEndpoint(); |
140 | 50 | } |
141 | | - } |
142 | | -}; |
143 | 51 |
|
144 | | -const fetchAndParseData = async (targetVersion: string) => { |
145 | | - // Fetch static.json and result.txt |
146 | | - console.time('Data fetch time'); |
147 | | - const [staticResponse, resultResponse] = await Promise.all([ |
148 | | - fetch(getStaticUrl(targetVersion)), |
149 | | - fetch(getResultUrl(targetVersion)), |
150 | | - ]); |
151 | | - if (!staticResponse.ok || !resultResponse.ok) { |
152 | | - throw new Error('Failed to fetch data'); |
| 52 | + setStatus({ |
| 53 | + type: OIerDbClientStatusEnum.InitializedPartially, |
| 54 | + text: swReady ? '等待 Service Worker 就绪' : '使用在线服务', |
| 55 | + }); |
| 56 | + } catch (error) { |
| 57 | + console.error('[Client] Failed to initialize:', error); |
| 58 | + setStatus({ |
| 59 | + type: OIerDbClientStatusEnum.Uninitialized, |
| 60 | + text: '初始化失败', |
| 61 | + }); |
| 62 | + throw error; |
153 | 63 | } |
154 | | - const [staticText, resultText] = await Promise.all([ |
155 | | - staticResponse.text(), |
156 | | - resultResponse.text(), |
157 | | - ]); |
158 | | - console.timeEnd('Data fetch time'); |
159 | | - |
160 | | - // Parse data |
161 | | - console.time('Data parse time'); |
162 | | - const parsedData = parseOIerDbData(resultText, staticText); |
163 | | - console.log('Parsed data version:', parsedData.data_version); |
164 | | - console.timeEnd('Data parse time'); |
165 | | - |
166 | | - return parsedData; |
167 | 64 | }; |
168 | 65 |
|
169 | | -const loadDataToIndexedDB = async ( |
170 | | - idbAdapter: IDBAdapter, |
171 | | - targetVersion: string, |
172 | | - isBackground: boolean, |
173 | | -) => { |
174 | | - const statusType = isBackground |
175 | | - ? OIerDbClientStatusEnum.InitializedPartially |
176 | | - : OIerDbClientStatusEnum.Initializing; |
177 | | - |
178 | | - // Fetch and parse data |
179 | | - setStatus({ |
180 | | - type: statusType, |
181 | | - text: isBackground ? '使用在线数据服务 [后台: 拉取并解析数据]' : '拉取并解析数据', |
182 | | - }); |
183 | | - const parsedData = await fetchAndParseData(targetVersion); |
| 66 | +const waitForSwAndSwitchEndpoint = () => { |
| 67 | + if (!navigator.serviceWorker) return; |
184 | 68 |
|
185 | | - // Save to IndexedDB |
186 | | - setStatus({ |
187 | | - type: statusType, |
188 | | - text: isBackground ? '使用在线数据服务 [后台: 保存到本地数据库]' : '保存到本地数据库', |
189 | | - }); |
190 | | - console.time('Data save time'); |
191 | | - await idbAdapter.loadData(parsedData); |
192 | | - console.timeEnd('Data save time'); |
193 | | -}; |
194 | | - |
195 | | -const loadDataInBackground = async (idbAdapter: IDBAdapter, targetVersion: string) => { |
196 | | - try { |
197 | | - await loadDataToIndexedDB(idbAdapter, targetVersion, true); |
198 | | - |
199 | | - // Switch to IDB adapter |
200 | | - globalThis.OIerDbClientInstance!.setAdapter(idbAdapter); |
201 | | - setStatus({ type: OIerDbClientStatusEnum.Initialized, text: '' }); |
202 | | - } catch (error) { |
203 | | - console.error('Failed to load data in background:', error); |
204 | | - // Keep using HTTP adapter |
205 | | - } |
206 | | -}; |
| 69 | + // Listen for SW to become activated first |
| 70 | + navigator.serviceWorker.ready.then((registration) => { |
| 71 | + console.log('[Client] SW activated, querying availability via postMessage'); |
207 | 72 |
|
208 | | -const loadDataFromStaticSource = async (idbAdapter: IDBAdapter, targetVersion: string) => { |
209 | | - await loadDataToIndexedDB(idbAdapter, targetVersion, false); |
| 73 | + let switched = false; |
| 74 | + let attempts = 0; |
| 75 | + const maxAttempts = 40; |
| 76 | + const timeoutMs = 20000; |
| 77 | + let timeoutId: number | null = null; |
210 | 78 |
|
211 | | - globalThis.OIerDbClientInstance = new OIerDbClient(idbAdapter); |
212 | | - setStatus({ type: OIerDbClientStatusEnum.Initialized, text: '' }); |
| 79 | + const cleanup = () => { |
| 80 | + navigator.serviceWorker.removeEventListener('message', handleMessage); |
| 81 | + if (timeoutId != null) { |
| 82 | + clearTimeout(timeoutId); |
| 83 | + } |
| 84 | + }; |
| 85 | + |
| 86 | + // Handler for SW status response |
| 87 | + const handleMessage = (event: MessageEvent) => { |
| 88 | + const { type, payload } = event.data || {}; |
| 89 | + |
| 90 | + if (type === 'statusResponse' && !switched) { |
| 91 | + console.log('[Client] SW status response:', payload); |
| 92 | + |
| 93 | + // Check if SW is ready (at least UsingHttp status means it can handle requests) |
| 94 | + if (payload.status >= SwStatusEnum.UsingHttp) { |
| 95 | + switched = true; |
| 96 | + cleanup(); |
| 97 | + |
| 98 | + console.log('[Client] SW is ready, switching to origin-based endpoint'); |
| 99 | + |
| 100 | + // Create new adapter with origin baseUrl |
| 101 | + const newAdapter = new HttpAdapter({ baseUrl: window.location.origin }); |
| 102 | + |
| 103 | + // Switch the client's adapter |
| 104 | + if (globalThis.OIerDbClientInstance) { |
| 105 | + globalThis.OIerDbClientInstance.setAdapter(newAdapter); |
| 106 | + } |
| 107 | + |
| 108 | + // Re-setup SW status listener |
| 109 | + setupSwStatusListener(); |
| 110 | + } else { |
| 111 | + // SW is still initializing, retry after a delay |
| 112 | + console.log('[Client] SW still initializing, retrying...'); |
| 113 | + attempts += 1; |
| 114 | + if (attempts >= maxAttempts) { |
| 115 | + console.warn('[Client] SW status polling reached max attempts'); |
| 116 | + cleanup(); |
| 117 | + return; |
| 118 | + } |
| 119 | + setTimeout(queryStatus, 500); |
| 120 | + } |
| 121 | + } |
| 122 | + }; |
| 123 | + |
| 124 | + // Query SW status |
| 125 | + const queryStatus = () => { |
| 126 | + const controller = registration.active; |
| 127 | + if (!controller) { |
| 128 | + console.log('[Client] SW not yet active, retrying...'); |
| 129 | + attempts += 1; |
| 130 | + if (attempts >= maxAttempts) { |
| 131 | + console.warn('[Client] SW activation polling reached max attempts'); |
| 132 | + cleanup(); |
| 133 | + return; |
| 134 | + } |
| 135 | + setTimeout(queryStatus, 100); |
| 136 | + return; |
| 137 | + } |
| 138 | + controller.postMessage({ type: 'getStatus' }); |
| 139 | + }; |
| 140 | + |
| 141 | + navigator.serviceWorker.addEventListener('message', handleMessage); |
| 142 | + timeoutId = window.setTimeout(() => { |
| 143 | + if (!switched) { |
| 144 | + console.warn('[Client] SW readiness polling timed out'); |
| 145 | + cleanup(); |
| 146 | + } |
| 147 | + }, timeoutMs); |
| 148 | + queryStatus(); |
| 149 | + }); |
213 | 150 | }; |
214 | 151 |
|
215 | 152 | export const initClient = () => { |
216 | 153 | // First set status to Initializing |
217 | 154 | setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '初始化数据查询模块' }); |
218 | 155 |
|
219 | 156 | // Then start async initialization, but don't wait for it |
220 | | - initClientAsync(); |
| 157 | + void initClientAsync().catch((error) => { |
| 158 | + console.error('[Client] initClientAsync rejected:', error); |
| 159 | + }); |
221 | 160 | }; |
222 | 161 |
|
223 | 162 | export const getClient = () => { |
|
0 commit comments