Skip to content

Commit b0d43a7

Browse files
committed
frontend: refactor to use service worker
1 parent 355621d commit b0d43a7

19 files changed

Lines changed: 3303 additions & 243 deletions

apps/frontend/app/entry.client.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { initClient } from '~/libs/client';
77
// This will initialize the OIerDb client and set up global state
88
initClient();
99

10+
// Install Service Worker
11+
if ('serviceWorker' in navigator) {
12+
navigator.serviceWorker.register(
13+
import.meta.env.MODE === 'production' ? '/sw.js' : '/dev-sw.js?dev-sw',
14+
{ type: import.meta.env.MODE === 'production' ? 'classic' : 'module' },
15+
);
16+
}
17+
1018
startTransition(() => {
1119
hydrateRoot(
1220
document,

apps/frontend/app/libs/client/client.ts

Lines changed: 127 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1,223 +1,162 @@
11
import { HttpAdapter } from '@oierdb/adapter-http';
2-
import { IDBAdapter } from '@oierdb/adapter-idb';
32
import { OIerDbClient } from '@oierdb/core';
4-
import { parseOIerDbData } from '@oierdb/parser';
53

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';
96

107
/**
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
1312
*
1413
* 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
3719
*/
3820
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: '初始化数据查询模块' });
4322

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);
4526

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);
4831

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 });
5933

60-
console.log('BackendAvailable:', backendAvailable, 'Version:', backendVersion);
34+
setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '检查服务可用性' });
6135

62-
// Get local IndexedDB version
63-
let localVersion = '';
64-
let localVersionAvailable = false;
6536
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);
7240

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);
10843

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();
14050
}
141-
}
142-
};
14351

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;
15363
}
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;
16764
};
16865

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;
18468

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');
20772

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;
21078

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+
});
213150
};
214151

215152
export const initClient = () => {
216153
// First set status to Initializing
217154
setStatus({ type: OIerDbClientStatusEnum.Initializing, text: '初始化数据查询模块' });
218155

219156
// 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+
});
221160
};
222161

223162
export const getClient = () => {

apps/frontend/app/libs/client/constant.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,3 @@ export const staticEndpoint = !import.meta.env.DEV
1616

1717
// Static data URLs
1818
export const staticDataVersionUrl = `${staticEndpoint}/version.json`;
19-
export const resultDataUrl = `${staticEndpoint}/result.txt`;
20-
export const staticDataUrl = `${staticEndpoint}/static.json`;

apps/frontend/app/libs/client/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,12 @@ declare global {
99
}
1010

1111
export { getClient, initClient } from './client';
12-
export { getStatus, subscribeToStatusChange, waitUntilClientReady } from './status';
12+
export {
13+
getStatus,
14+
getSwStatus,
15+
querySwStatus,
16+
subscribeToStatusChange,
17+
SwStatusEnum,
18+
waitUntilClientReady,
19+
} from './status';
20+
export type { SwStatus } from './status';

0 commit comments

Comments
 (0)