-
-
Notifications
You must be signed in to change notification settings - Fork 108
Expand file tree
/
Copy pathevents.ts
More file actions
196 lines (176 loc) · 6.11 KB
/
Copy pathevents.ts
File metadata and controls
196 lines (176 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import type { ConnectionType } from '@conar/shared/enums/connection-type'
import { decrypt, encrypt } from '@conar/shared/utils/encryption'
import { tryParseJson } from '@conar/shared/utils/helpers'
import { app, ipcMain } from 'electron'
import { autoUpdater, sendToast } from '..'
import { getClient as getClickhouseClient } from '../connections/clickhouse'
import { getPool as getMssqlPool } from '../connections/mssql'
import { getPool as getMysqlPool } from '../connections/mysql'
import { getPool as getPgPool } from '../connections/pg'
function isConnectionError(error: unknown) {
if (error instanceof Error) {
if (
error.message.includes('ECONNRESET')
|| error.message.toLowerCase().includes('connection lost')
) {
return true
}
}
return false
}
const MAX_RECONNECTION_ATTEMPTS = 5
const RECONNECTION_DELAY = 3000
async function retryIfConnectionError<T>(func: () => Promise<T>, {
onRetry,
onError,
onSuccess,
attempt = 0,
}: {
onRetry?: (props: { attempt: number }) => void
onError?: (error: unknown) => void
onSuccess?: (props: { attempt: number }) => void
attempt?: number
} = {
attempt: 0,
}): Promise<T> {
try {
const result = await func()
onSuccess?.({ attempt })
return result
}
catch (error) {
if (isConnectionError(error)) {
if (attempt < MAX_RECONNECTION_ATTEMPTS) {
await new Promise(resolve => setTimeout(resolve, RECONNECTION_DELAY))
onRetry?.({ attempt: attempt + 1 })
return retryIfConnectionError(func, {
attempt: attempt + 1,
onError,
onRetry,
onSuccess,
})
}
onError?.(error)
}
throw error
}
}
function retryOptions({ silent }: { silent?: boolean }) {
return {
onSuccess: ({ attempt }) => {
if (attempt > 0 && !silent) {
sendToast({ message: `Database connection successful after reconnection ${attempt} attempt${attempt > 1 ? 's' : ''}.`, type: 'success' })
}
},
onRetry({ attempt }) {
if (!silent) {
sendToast({ message: `Could not connect to the database. Reconnection attempt ${attempt + 1}/${MAX_RECONNECTION_ATTEMPTS}.`, type: 'info' })
}
},
onError: () => {
if (!silent) {
sendToast({ message: 'Could not connect to the database. Please check your network or database server and try again.', type: 'error' })
}
},
} satisfies Parameters<typeof retryIfConnectionError>[1]
}
const queryMap = {
postgres: async ({ connectionString, query, values, silent }: { query: string, values: unknown[], connectionString: string, silent?: boolean }) => {
let start = 0
const result = await retryIfConnectionError(async () => {
const pool = await getPgPool(connectionString)
start = performance.now()
return pool.query(query, values)
}, retryOptions({ silent }))
return { result: result.rows as unknown, duration: performance.now() - start }
},
mysql: async ({ connectionString, query, values, silent }: { query: string, values: unknown[], connectionString: string, silent?: boolean }) => {
let start = 0
const [result] = await retryIfConnectionError(async () => {
const pool = await getMysqlPool(connectionString)
start = performance.now()
return pool.query(query, values)
}, retryOptions({ silent }))
return { result: result as unknown, duration: performance.now() - start! }
},
clickhouse: async ({ connectionString, query, silent }: { query: string, connectionString: string, insertValues?: unknown[], silent?: boolean }) => {
try {
const client = getClickhouseClient(connectionString)
const isSelect = [
'SELECT',
'SHOW',
'DESCRIBE',
'EXPLAIN',
'WITH',
'CHECK',
].some(keyword => query.trim().toUpperCase().startsWith(keyword))
let start = 0
if (isSelect) {
const result = await retryIfConnectionError(() => {
start = performance.now()
return client.query({ query, format: 'JSONEachRow' }).then(result => result.json())
}, retryOptions({ silent }))
return { result, duration: performance.now() - start }
}
await retryIfConnectionError(() => {
start = performance.now()
return client.exec({ query })
}, retryOptions({ silent }))
return { result: [], duration: performance.now() - start }
}
catch (error) {
if (error instanceof Error) {
const parsed = tryParseJson<Partial<{ message: string, status: string, code: number, request_id: string }>>(error.message)
if (parsed?.message) {
throw new Error(parsed.message, { cause: error })
}
}
throw error
}
},
mssql: async ({ connectionString, query, values, silent }: { query: string, values: unknown[], connectionString: string, silent?: boolean }) => {
let start = 0
const result = await retryIfConnectionError(async () => {
const pool = await getMssqlPool(connectionString)
let request = pool.request()
for (let i = 0; i < values.length; i++) {
request = request.input(`${i + 1}`, values[i])
}
start = performance.now()
return request.query(query)
}, retryOptions({ silent }))
return { result: result.recordset as unknown, duration: performance.now() - start! }
},
// eslint-disable-next-line ts/no-explicit-any
} satisfies Record<ConnectionType, (...args: any[]) => Promise<{
result: unknown
duration: number
}>>
const encryption = {
encrypt: async (arg: Parameters<typeof encrypt>[0]) => encrypt(arg),
decrypt: async (arg: Parameters<typeof decrypt>[0]) => decrypt(arg),
}
const _app = {
checkForUpdates: () => {
return autoUpdater?.checkForUpdates()
},
quitAndInstall: () => {
autoUpdater?.restartAndInstall()
},
}
const versions = {
app: async () => app.getVersion(),
}
export const electron = {
query: queryMap,
encryption,
app: _app,
versions,
}
export function initElectronEvents() {
for (const [key, events] of Object.entries(electron)) {
for (const [key2, handler] of Object.entries(events)) {
ipcMain.handle(`${key}.${key2}`, (_event, arg) => handler(arg))
}
}
}