@lobehub/chat 1.36.46 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.ja-JP.md +8 -8
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/changelog/v1.json +9 -0
- package/next.config.mjs +4 -1
- package/package.json +5 -3
- package/scripts/migrateClientDB/compile-migrations.ts +14 -0
- package/src/app/(main)/(mobile)/me/(home)/layout.tsx +2 -0
- package/src/app/(main)/chat/_layout/Desktop/index.tsx +3 -2
- package/src/app/(main)/chat/_layout/Mobile.tsx +5 -3
- package/src/app/(main)/chat/features/Migration/DBReader.ts +290 -0
- package/src/app/(main)/chat/features/Migration/UpgradeButton.tsx +4 -8
- package/src/app/(main)/chat/features/Migration/index.tsx +26 -15
- package/src/app/(main)/settings/_layout/Desktop/index.tsx +2 -0
- package/src/app/loading/Client/Content.tsx +11 -1
- package/src/app/loading/Client/Error.tsx +27 -0
- package/src/app/loading/stage.ts +8 -0
- package/src/components/FullscreenLoading/index.tsx +4 -3
- package/src/const/version.ts +1 -0
- package/src/database/client/db.test.ts +172 -0
- package/src/database/client/db.ts +246 -0
- package/src/database/client/migrations.json +289 -0
- package/src/features/InitClientDB/EnableModal.tsx +111 -0
- package/src/features/InitClientDB/ErrorResult.tsx +125 -0
- package/src/features/InitClientDB/InitIndicator.tsx +124 -0
- package/src/features/InitClientDB/PGliteSVG.tsx +22 -0
- package/src/features/InitClientDB/index.tsx +37 -0
- package/src/hooks/useCheckPluginsIsInstalled.ts +2 -2
- package/src/hooks/useFetchInstalledPlugins.ts +2 -2
- package/src/hooks/useFetchMessages.ts +2 -2
- package/src/hooks/useFetchSessions.ts +2 -2
- package/src/hooks/useFetchThreads.ts +2 -2
- package/src/hooks/useFetchTopics.ts +2 -2
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -2
- package/src/services/baseClientService/index.ts +9 -0
- package/src/services/debug.ts +32 -34
- package/src/services/file/index.ts +6 -2
- package/src/services/file/pglite.test.ts +198 -0
- package/src/services/file/pglite.ts +84 -0
- package/src/services/file/type.ts +4 -3
- package/src/services/github.ts +17 -0
- package/src/services/import/index.ts +6 -2
- package/src/services/import/pglite.test.ts +997 -0
- package/src/services/import/pglite.ts +34 -0
- package/src/services/message/client.ts +2 -0
- package/src/services/message/index.ts +6 -2
- package/src/services/message/pglite.test.ts +430 -0
- package/src/services/message/pglite.ts +118 -0
- package/src/services/message/server.ts +9 -9
- package/src/services/message/type.ts +3 -4
- package/src/services/plugin/index.ts +6 -2
- package/src/services/plugin/pglite.test.ts +175 -0
- package/src/services/plugin/pglite.ts +51 -0
- package/src/services/session/client.ts +1 -1
- package/src/services/session/index.ts +6 -2
- package/src/services/session/pglite.test.ts +411 -0
- package/src/services/session/pglite.ts +184 -0
- package/src/services/session/type.ts +14 -1
- package/src/services/topic/index.ts +6 -3
- package/src/services/topic/pglite.test.ts +212 -0
- package/src/services/topic/pglite.ts +85 -0
- package/src/services/user/client.test.ts +0 -1
- package/src/services/user/index.ts +8 -2
- package/src/services/user/pglite.test.ts +98 -0
- package/src/services/user/pglite.ts +92 -0
- package/src/store/chat/slices/builtinTool/action.test.ts +3 -4
- package/src/store/global/actions/clientDb.ts +51 -0
- package/src/store/global/initialState.ts +13 -0
- package/src/store/global/selectors.ts +24 -3
- package/src/store/global/store.ts +3 -1
- package/src/store/session/slices/sessionGroup/reducer.test.ts +6 -6
- package/src/store/user/slices/common/action.ts +2 -4
- package/src/types/clientDB.ts +29 -0
- package/src/types/importer.ts +17 -5
- package/src/types/meta.ts +0 -9
- package/src/types/session/sessionGroup.ts +3 -3
- package/src/services/message/index.test.ts +0 -48
@@ -6,6 +6,7 @@ import { memo, useRef } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
8
8
|
|
9
|
+
import InitClientDB from '@/features/InitClientDB';
|
9
10
|
import Footer from '@/features/Setting/Footer';
|
10
11
|
import SettingContainer from '@/features/Setting/SettingContainer';
|
11
12
|
import { useActiveSettingsKey } from '@/hooks/useActiveSettingsKey';
|
@@ -45,6 +46,7 @@ const Layout = memo<LayoutProps>(({ children, category }) => {
|
|
45
46
|
</Header>
|
46
47
|
)}
|
47
48
|
<SettingContainer addonAfter={<Footer />}>{children}</SettingContainer>
|
49
|
+
<InitClientDB />
|
48
50
|
</Flexbox>
|
49
51
|
);
|
50
52
|
});
|
@@ -1,17 +1,25 @@
|
|
1
|
+
import dynamic from 'next/dynamic';
|
1
2
|
import React, { memo } from 'react';
|
2
3
|
import { useTranslation } from 'react-i18next';
|
3
4
|
|
4
5
|
import FullscreenLoading from '@/components/FullscreenLoading';
|
5
6
|
import { useGlobalStore } from '@/store/global';
|
6
7
|
import { systemStatusSelectors } from '@/store/global/selectors';
|
8
|
+
import { DatabaseLoadingState } from '@/types/clientDB';
|
7
9
|
|
8
10
|
import { CLIENT_LOADING_STAGES } from '../stage';
|
9
11
|
|
12
|
+
const InitError = dynamic(() => import('./Error'), { ssr: false });
|
13
|
+
|
10
14
|
interface InitProps {
|
11
15
|
setActiveStage: (value: string) => void;
|
12
16
|
}
|
13
17
|
|
14
|
-
const Init = memo<InitProps>(() => {
|
18
|
+
const Init = memo<InitProps>(({ setActiveStage }) => {
|
19
|
+
const useInitClientDB = useGlobalStore((s) => s.useInitClientDB);
|
20
|
+
|
21
|
+
useInitClientDB({ onStateChange: setActiveStage });
|
22
|
+
|
15
23
|
return null;
|
16
24
|
});
|
17
25
|
|
@@ -23,12 +31,14 @@ interface ContentProps {
|
|
23
31
|
const Content = memo<ContentProps>(({ loadingStage, setActiveStage }) => {
|
24
32
|
const { t } = useTranslation('common');
|
25
33
|
const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited);
|
34
|
+
const isError = useGlobalStore((s) => s.initClientDBStage === DatabaseLoadingState.Error);
|
26
35
|
|
27
36
|
return (
|
28
37
|
<>
|
29
38
|
{isPgliteNotInited && <Init setActiveStage={setActiveStage} />}
|
30
39
|
<FullscreenLoading
|
31
40
|
activeStage={CLIENT_LOADING_STAGES.indexOf(loadingStage)}
|
41
|
+
contentRender={isError && <InitError />}
|
32
42
|
stages={CLIENT_LOADING_STAGES.map((key) => t(`appLoading.${key}` as any))}
|
33
43
|
/>
|
34
44
|
</>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import { Button } from 'antd';
|
2
|
+
import React from 'react';
|
3
|
+
import { useTranslation } from 'react-i18next';
|
4
|
+
import { Center } from 'react-layout-kit';
|
5
|
+
|
6
|
+
import ErrorResult from '@/features/InitClientDB/ErrorResult';
|
7
|
+
|
8
|
+
const InitError = () => {
|
9
|
+
const { t } = useTranslation('common');
|
10
|
+
|
11
|
+
return (
|
12
|
+
<ErrorResult>
|
13
|
+
{({ setOpen }) => (
|
14
|
+
<Center gap={8}>
|
15
|
+
{t('appLoading.failed')}
|
16
|
+
<div>
|
17
|
+
<Button onClick={() => setOpen(true)} type={'primary'}>
|
18
|
+
{t('appLoading.showDetail')}
|
19
|
+
</Button>
|
20
|
+
</div>
|
21
|
+
</Center>
|
22
|
+
)}
|
23
|
+
</ErrorResult>
|
24
|
+
);
|
25
|
+
};
|
26
|
+
|
27
|
+
export default InitError;
|
package/src/app/loading/stage.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import { DatabaseLoadingState } from '@/types/clientDB';
|
2
|
+
|
1
3
|
export enum AppLoadingStage {
|
2
4
|
GoToChat = 'goToChat',
|
3
5
|
Idle = 'appIdle',
|
@@ -17,6 +19,12 @@ export const SERVER_LOADING_STAGES = [
|
|
17
19
|
export const CLIENT_LOADING_STAGES = [
|
18
20
|
AppLoadingStage.Idle,
|
19
21
|
AppLoadingStage.Initializing,
|
22
|
+
DatabaseLoadingState.Initializing,
|
23
|
+
DatabaseLoadingState.LoadingDependencies,
|
24
|
+
DatabaseLoadingState.LoadingWasm,
|
25
|
+
DatabaseLoadingState.Migrating,
|
26
|
+
DatabaseLoadingState.Finished,
|
27
|
+
DatabaseLoadingState.Ready,
|
20
28
|
AppLoadingStage.InitUser,
|
21
29
|
AppLoadingStage.GoToChat,
|
22
30
|
] as string[];
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { memo } from 'react';
|
1
|
+
import React, { ReactNode, memo } from 'react';
|
2
2
|
import { Center, Flexbox } from 'react-layout-kit';
|
3
3
|
|
4
4
|
import { ProductLogo } from '@/components/Branding';
|
@@ -6,15 +6,16 @@ import InitProgress, { StageItem } from '@/components/InitProgress';
|
|
6
6
|
|
7
7
|
interface FullscreenLoadingProps {
|
8
8
|
activeStage: number;
|
9
|
+
contentRender?: ReactNode;
|
9
10
|
stages: StageItem[];
|
10
11
|
}
|
11
12
|
|
12
|
-
const FullscreenLoading = memo<FullscreenLoadingProps>(({ activeStage, stages }) => {
|
13
|
+
const FullscreenLoading = memo<FullscreenLoadingProps>(({ activeStage, stages, contentRender }) => {
|
13
14
|
return (
|
14
15
|
<Flexbox height={'100%'} style={{ position: 'relative', userSelect: 'none' }} width={'100%'}>
|
15
16
|
<Center flex={1} gap={16} width={'100%'}>
|
16
17
|
<ProductLogo size={48} type={'combine'} />
|
17
|
-
<InitProgress activeStage={activeStage} stages={stages} />
|
18
|
+
{contentRender ? contentRender : <InitProgress activeStage={activeStage} stages={stages} />}
|
18
19
|
</Center>
|
19
20
|
</Flexbox>
|
20
21
|
);
|
package/src/const/version.ts
CHANGED
@@ -6,6 +6,7 @@ import { BRANDING_NAME, ORG_NAME } from './branding';
|
|
6
6
|
export const CURRENT_VERSION = pkg.version;
|
7
7
|
|
8
8
|
export const isServerMode = getServerDBConfig().NEXT_PUBLIC_ENABLED_SERVER_SERVICE;
|
9
|
+
export const isUsePgliteDB = process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite';
|
9
10
|
|
10
11
|
// @ts-ignore
|
11
12
|
export const isCustomBranding = BRANDING_NAME !== 'LobeChat';
|
@@ -0,0 +1,172 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB';
|
4
|
+
|
5
|
+
import { DatabaseManager } from './db';
|
6
|
+
|
7
|
+
// Mock 所有外部依赖
|
8
|
+
vi.mock('@electric-sql/pglite', () => ({
|
9
|
+
default: vi.fn(),
|
10
|
+
IdbFs: vi.fn(),
|
11
|
+
PGlite: vi.fn(),
|
12
|
+
MemoryFS: vi.fn(),
|
13
|
+
}));
|
14
|
+
|
15
|
+
vi.mock('@electric-sql/pglite/vector', () => ({
|
16
|
+
default: vi.fn(),
|
17
|
+
vector: vi.fn(),
|
18
|
+
}));
|
19
|
+
|
20
|
+
vi.mock('drizzle-orm/pglite', () => ({
|
21
|
+
drizzle: vi.fn(() => ({
|
22
|
+
dialect: {
|
23
|
+
migrate: vi.fn().mockResolvedValue(undefined),
|
24
|
+
},
|
25
|
+
})),
|
26
|
+
}));
|
27
|
+
|
28
|
+
let manager: DatabaseManager;
|
29
|
+
let progressEvents: ClientDBLoadingProgress[] = [];
|
30
|
+
let stateChanges: DatabaseLoadingState[] = [];
|
31
|
+
|
32
|
+
let callbacks = {
|
33
|
+
onProgress: vi.fn((progress: ClientDBLoadingProgress) => {
|
34
|
+
progressEvents.push(progress);
|
35
|
+
}),
|
36
|
+
onStateChange: vi.fn((state: DatabaseLoadingState) => {
|
37
|
+
stateChanges.push(state);
|
38
|
+
}),
|
39
|
+
};
|
40
|
+
|
41
|
+
beforeEach(() => {
|
42
|
+
vi.clearAllMocks();
|
43
|
+
progressEvents = [];
|
44
|
+
stateChanges = [];
|
45
|
+
|
46
|
+
callbacks = {
|
47
|
+
onProgress: vi.fn((progress: ClientDBLoadingProgress) => {
|
48
|
+
progressEvents.push(progress);
|
49
|
+
}),
|
50
|
+
onStateChange: vi.fn((state: DatabaseLoadingState) => {
|
51
|
+
stateChanges.push(state);
|
52
|
+
}),
|
53
|
+
};
|
54
|
+
// @ts-expect-error
|
55
|
+
DatabaseManager['instance'] = undefined;
|
56
|
+
manager = DatabaseManager.getInstance();
|
57
|
+
});
|
58
|
+
|
59
|
+
describe('DatabaseManager', () => {
|
60
|
+
describe('Callback Handling', () => {
|
61
|
+
it('should properly track loading states', async () => {
|
62
|
+
await manager.initialize(callbacks);
|
63
|
+
|
64
|
+
// 验证状态转换顺序
|
65
|
+
expect(stateChanges).toEqual([
|
66
|
+
DatabaseLoadingState.Initializing,
|
67
|
+
DatabaseLoadingState.LoadingDependencies,
|
68
|
+
DatabaseLoadingState.LoadingWasm,
|
69
|
+
DatabaseLoadingState.Migrating,
|
70
|
+
DatabaseLoadingState.Finished,
|
71
|
+
DatabaseLoadingState.Ready,
|
72
|
+
]);
|
73
|
+
});
|
74
|
+
|
75
|
+
it('should report dependencies loading progress', async () => {
|
76
|
+
await manager.initialize(callbacks);
|
77
|
+
|
78
|
+
// 验证依赖加载进度回调
|
79
|
+
const dependencyProgress = progressEvents.filter((e) => e.phase === 'dependencies');
|
80
|
+
expect(dependencyProgress.length).toBeGreaterThan(0);
|
81
|
+
expect(dependencyProgress[dependencyProgress.length - 1]).toEqual(
|
82
|
+
expect.objectContaining({
|
83
|
+
phase: 'dependencies',
|
84
|
+
progress: 100,
|
85
|
+
costTime: expect.any(Number),
|
86
|
+
}),
|
87
|
+
);
|
88
|
+
});
|
89
|
+
|
90
|
+
it('should report WASM loading progress', async () => {
|
91
|
+
await manager.initialize(callbacks);
|
92
|
+
|
93
|
+
// 验证 WASM 加载进度回调
|
94
|
+
const wasmProgress = progressEvents.filter((e) => e.phase === 'wasm');
|
95
|
+
// expect(wasmProgress.length).toBeGreaterThan(0);
|
96
|
+
expect(wasmProgress[wasmProgress.length - 1]).toEqual(
|
97
|
+
expect.objectContaining({
|
98
|
+
phase: 'wasm',
|
99
|
+
progress: 100,
|
100
|
+
costTime: expect.any(Number),
|
101
|
+
}),
|
102
|
+
);
|
103
|
+
});
|
104
|
+
|
105
|
+
it('should handle initialization errors', async () => {
|
106
|
+
// 模拟加载失败
|
107
|
+
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
|
108
|
+
|
109
|
+
await expect(manager.initialize(callbacks)).rejects.toThrow();
|
110
|
+
expect(stateChanges).toContain(DatabaseLoadingState.Error);
|
111
|
+
});
|
112
|
+
|
113
|
+
it('should only initialize once when called multiple times', async () => {
|
114
|
+
const firstInit = manager.initialize(callbacks);
|
115
|
+
const secondInit = manager.initialize(callbacks);
|
116
|
+
|
117
|
+
await Promise.all([firstInit, secondInit]);
|
118
|
+
|
119
|
+
// 验证回调只被调用一次
|
120
|
+
const readyStateCount = stateChanges.filter(
|
121
|
+
(state) => state === DatabaseLoadingState.Ready,
|
122
|
+
).length;
|
123
|
+
expect(readyStateCount).toBe(1);
|
124
|
+
});
|
125
|
+
});
|
126
|
+
|
127
|
+
describe('Progress Calculation', () => {
|
128
|
+
it('should report progress between 0 and 100', async () => {
|
129
|
+
await manager.initialize(callbacks);
|
130
|
+
|
131
|
+
// 验证所有进度值都在有效范围内
|
132
|
+
progressEvents.forEach((event) => {
|
133
|
+
expect(event.progress).toBeGreaterThanOrEqual(0);
|
134
|
+
expect(event.progress).toBeLessThanOrEqual(100);
|
135
|
+
});
|
136
|
+
});
|
137
|
+
|
138
|
+
it('should include timing information', async () => {
|
139
|
+
await manager.initialize(callbacks);
|
140
|
+
|
141
|
+
// 验证最终进度回调包含耗时信息
|
142
|
+
const finalProgress = progressEvents[progressEvents.length - 1];
|
143
|
+
expect(finalProgress.costTime).toBeGreaterThan(0);
|
144
|
+
});
|
145
|
+
});
|
146
|
+
|
147
|
+
describe('Error Handling', () => {
|
148
|
+
it('should handle missing callbacks gracefully', async () => {
|
149
|
+
// 测试没有提供回调的情况
|
150
|
+
await expect(manager.initialize()).resolves.toBeDefined();
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should handle partial callbacks', async () => {
|
154
|
+
// 只提供部分回调
|
155
|
+
await expect(manager.initialize({ onProgress: callbacks.onProgress })).resolves.toBeDefined();
|
156
|
+
await expect(
|
157
|
+
manager.initialize({ onStateChange: callbacks.onStateChange }),
|
158
|
+
).resolves.toBeDefined();
|
159
|
+
});
|
160
|
+
});
|
161
|
+
|
162
|
+
describe('Database Access', () => {
|
163
|
+
it('should throw error when accessing database before initialization', () => {
|
164
|
+
expect(() => manager.db).toThrow('Database not initialized');
|
165
|
+
});
|
166
|
+
|
167
|
+
it('should provide access to database after initialization', async () => {
|
168
|
+
await manager.initialize();
|
169
|
+
expect(() => manager.db).not.toThrow();
|
170
|
+
});
|
171
|
+
});
|
172
|
+
});
|
@@ -0,0 +1,246 @@
|
|
1
|
+
import type { PgliteDatabase } from 'drizzle-orm/pglite';
|
2
|
+
import { Md5 } from 'ts-md5';
|
3
|
+
|
4
|
+
import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB';
|
5
|
+
import { sleep } from '@/utils/sleep';
|
6
|
+
|
7
|
+
import * as schema from '../schemas';
|
8
|
+
import migrations from './migrations.json';
|
9
|
+
|
10
|
+
const pgliteSchemaHashCache = 'LOBE_CHAT_PGLITE_SCHEMA_HASH';
|
11
|
+
|
12
|
+
type DrizzleInstance = PgliteDatabase<typeof schema>;
|
13
|
+
|
14
|
+
export interface DatabaseLoadingCallbacks {
|
15
|
+
onError?: (error: Error) => void;
|
16
|
+
onProgress?: (progress: ClientDBLoadingProgress) => void;
|
17
|
+
onStateChange?: (state: DatabaseLoadingState) => void;
|
18
|
+
}
|
19
|
+
|
20
|
+
export class DatabaseManager {
|
21
|
+
private static instance: DatabaseManager;
|
22
|
+
private dbInstance: DrizzleInstance | null = null;
|
23
|
+
private initPromise: Promise<DrizzleInstance> | null = null;
|
24
|
+
private callbacks?: DatabaseLoadingCallbacks;
|
25
|
+
private isLocalDBSchemaSynced = false;
|
26
|
+
|
27
|
+
// CDN 配置
|
28
|
+
private static WASM_CDN_URL =
|
29
|
+
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.13/files/dist/postgres.wasm';
|
30
|
+
|
31
|
+
private constructor() {}
|
32
|
+
|
33
|
+
static getInstance() {
|
34
|
+
if (!DatabaseManager.instance) {
|
35
|
+
DatabaseManager.instance = new DatabaseManager();
|
36
|
+
}
|
37
|
+
return DatabaseManager.instance;
|
38
|
+
}
|
39
|
+
|
40
|
+
// 加载并编译 WASM 模块
|
41
|
+
private async loadWasmModule(): Promise<WebAssembly.Module> {
|
42
|
+
const start = Date.now();
|
43
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingWasm);
|
44
|
+
|
45
|
+
const response = await fetch(DatabaseManager.WASM_CDN_URL);
|
46
|
+
|
47
|
+
const contentLength = Number(response.headers.get('Content-Length')) || 0;
|
48
|
+
const reader = response.body?.getReader();
|
49
|
+
|
50
|
+
if (!reader) throw new Error('Failed to start WASM download');
|
51
|
+
|
52
|
+
let receivedLength = 0;
|
53
|
+
const chunks: Uint8Array[] = [];
|
54
|
+
|
55
|
+
// 读取数据流
|
56
|
+
// eslint-disable-next-line no-constant-condition
|
57
|
+
while (true) {
|
58
|
+
const { done, value } = await reader.read();
|
59
|
+
|
60
|
+
if (done) break;
|
61
|
+
|
62
|
+
chunks.push(value);
|
63
|
+
receivedLength += value.length;
|
64
|
+
|
65
|
+
// 计算并报告进度
|
66
|
+
const progress = Math.min(Math.round((receivedLength / contentLength) * 100), 100);
|
67
|
+
this.callbacks?.onProgress?.({
|
68
|
+
phase: 'wasm',
|
69
|
+
progress,
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
// 合并数据块
|
74
|
+
const wasmBytes = new Uint8Array(receivedLength);
|
75
|
+
let position = 0;
|
76
|
+
for (const chunk of chunks) {
|
77
|
+
wasmBytes.set(chunk, position);
|
78
|
+
position += chunk.length;
|
79
|
+
}
|
80
|
+
|
81
|
+
this.callbacks?.onProgress?.({
|
82
|
+
costTime: Date.now() - start,
|
83
|
+
phase: 'wasm',
|
84
|
+
progress: 100,
|
85
|
+
});
|
86
|
+
|
87
|
+
// 编译 WASM 模块
|
88
|
+
return WebAssembly.compile(wasmBytes);
|
89
|
+
}
|
90
|
+
|
91
|
+
// 异步加载 PGlite 相关依赖
|
92
|
+
private async loadDependencies() {
|
93
|
+
const start = Date.now();
|
94
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingDependencies);
|
95
|
+
|
96
|
+
const imports = [
|
97
|
+
import('@electric-sql/pglite').then((m) => ({
|
98
|
+
IdbFs: m.IdbFs,
|
99
|
+
MemoryFS: m.MemoryFS,
|
100
|
+
PGlite: m.PGlite,
|
101
|
+
})),
|
102
|
+
import('@electric-sql/pglite/vector'),
|
103
|
+
import('drizzle-orm/pglite'),
|
104
|
+
];
|
105
|
+
|
106
|
+
let loaded = 0;
|
107
|
+
const results = await Promise.all(
|
108
|
+
imports.map(async (importPromise) => {
|
109
|
+
const result = await importPromise;
|
110
|
+
loaded += 1;
|
111
|
+
|
112
|
+
// 计算加载进度
|
113
|
+
this.callbacks?.onProgress?.({
|
114
|
+
phase: 'dependencies',
|
115
|
+
progress: Math.min(Math.round((loaded / imports.length) * 100), 100),
|
116
|
+
});
|
117
|
+
return result;
|
118
|
+
}),
|
119
|
+
);
|
120
|
+
|
121
|
+
this.callbacks?.onProgress?.({
|
122
|
+
costTime: Date.now() - start,
|
123
|
+
phase: 'dependencies',
|
124
|
+
progress: 100,
|
125
|
+
});
|
126
|
+
|
127
|
+
// @ts-ignore
|
128
|
+
const [{ PGlite, IdbFs, MemoryFS }, { vector }, { drizzle }] = results;
|
129
|
+
|
130
|
+
return { IdbFs, MemoryFS, PGlite, drizzle, vector };
|
131
|
+
}
|
132
|
+
|
133
|
+
// 数据库迁移方法
|
134
|
+
private async migrate(skipMultiRun = false): Promise<DrizzleInstance> {
|
135
|
+
if (this.isLocalDBSchemaSynced && skipMultiRun) return this.db;
|
136
|
+
|
137
|
+
const cacheHash = localStorage.getItem(pgliteSchemaHashCache);
|
138
|
+
const hash = Md5.hashStr(JSON.stringify(migrations));
|
139
|
+
|
140
|
+
// if hash is the same, no need to migrate
|
141
|
+
if (hash === cacheHash) {
|
142
|
+
this.isLocalDBSchemaSynced = true;
|
143
|
+
return this.db;
|
144
|
+
}
|
145
|
+
|
146
|
+
const start = Date.now();
|
147
|
+
try {
|
148
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.Migrating);
|
149
|
+
|
150
|
+
// refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532
|
151
|
+
// @ts-expect-error
|
152
|
+
await this.db.dialect.migrate(migrations, this.db.session, {});
|
153
|
+
localStorage.setItem(pgliteSchemaHashCache, hash);
|
154
|
+
this.isLocalDBSchemaSynced = true;
|
155
|
+
|
156
|
+
console.info(`🗂 Migration success, take ${Date.now() - start}ms`);
|
157
|
+
} catch (cause) {
|
158
|
+
console.error('❌ Local database schema migration failed', cause);
|
159
|
+
throw cause;
|
160
|
+
}
|
161
|
+
|
162
|
+
return this.db;
|
163
|
+
}
|
164
|
+
|
165
|
+
// 初始化数据库
|
166
|
+
async initialize(callbacks?: DatabaseLoadingCallbacks): Promise<DrizzleInstance> {
|
167
|
+
if (this.initPromise) return this.initPromise;
|
168
|
+
|
169
|
+
this.callbacks = callbacks;
|
170
|
+
|
171
|
+
this.initPromise = (async () => {
|
172
|
+
try {
|
173
|
+
if (this.dbInstance) return this.dbInstance;
|
174
|
+
|
175
|
+
const time = Date.now();
|
176
|
+
// 初始化数据库
|
177
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.Initializing);
|
178
|
+
|
179
|
+
// 加载依赖
|
180
|
+
const { PGlite, vector, drizzle, IdbFs, MemoryFS } = await this.loadDependencies();
|
181
|
+
|
182
|
+
// 加载并编译 WASM 模块
|
183
|
+
const wasmModule = await this.loadWasmModule();
|
184
|
+
|
185
|
+
const db = new PGlite({
|
186
|
+
extensions: { vector },
|
187
|
+
fs: typeof window === 'undefined' ? new MemoryFS('lobechat') : new IdbFs('lobechat'),
|
188
|
+
relaxedDurability: true,
|
189
|
+
wasmModule,
|
190
|
+
});
|
191
|
+
|
192
|
+
this.dbInstance = drizzle({ client: db, schema });
|
193
|
+
|
194
|
+
await this.migrate(true);
|
195
|
+
|
196
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.Finished);
|
197
|
+
console.log(`✅ Database initialized in ${Date.now() - time}ms`);
|
198
|
+
|
199
|
+
await sleep(50);
|
200
|
+
|
201
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.Ready);
|
202
|
+
|
203
|
+
return this.dbInstance as DrizzleInstance;
|
204
|
+
} catch (e) {
|
205
|
+
this.initPromise = null;
|
206
|
+
this.callbacks?.onStateChange?.(DatabaseLoadingState.Error);
|
207
|
+
const error = e as Error;
|
208
|
+
this.callbacks?.onError?.({
|
209
|
+
message: error.message,
|
210
|
+
name: error.name,
|
211
|
+
stack: error.stack,
|
212
|
+
});
|
213
|
+
throw error;
|
214
|
+
}
|
215
|
+
})();
|
216
|
+
|
217
|
+
return this.initPromise;
|
218
|
+
}
|
219
|
+
|
220
|
+
// 获取数据库实例
|
221
|
+
get db(): DrizzleInstance {
|
222
|
+
if (!this.dbInstance) {
|
223
|
+
throw new Error('Database not initialized. Please call initialize() first.');
|
224
|
+
}
|
225
|
+
return this.dbInstance;
|
226
|
+
}
|
227
|
+
|
228
|
+
// 创建代理对象
|
229
|
+
createProxy(): DrizzleInstance {
|
230
|
+
return new Proxy({} as DrizzleInstance, {
|
231
|
+
get: (target, prop) => {
|
232
|
+
return this.db[prop as keyof DrizzleInstance];
|
233
|
+
},
|
234
|
+
});
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
// 导出单例
|
239
|
+
const dbManager = DatabaseManager.getInstance();
|
240
|
+
|
241
|
+
// 保持原有的 clientDB 导出不变
|
242
|
+
export const clientDB = dbManager.createProxy();
|
243
|
+
|
244
|
+
// 导出初始化方法,供应用启动时使用
|
245
|
+
export const initializeDB = (callbacks?: DatabaseLoadingCallbacks) =>
|
246
|
+
dbManager.initialize(callbacks);
|