database-devtools 0.1.1 → 0.1.2
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/dist/DatabaseDevTools-CdpQJBqG.d.ts +39 -0
- package/dist/DatabaseDevTools-sw1wX5nW.d.cts +39 -0
- package/dist/adapter/index.d.cts +3 -3
- package/dist/adapter/index.d.ts +3 -3
- package/dist/{adapter-BjJm71wW.d.cts → adapter-DQyQ5ycj.d.cts} +1 -1
- package/dist/{adapter-DJSjDewg.d.ts → adapter-cIF5P6qI.d.ts} +1 -1
- package/dist/adapters/sqlite/index.d.cts +4 -4
- package/dist/adapters/sqlite/index.d.ts +4 -4
- package/dist/chunk-7UY2LDRW.js +2739 -0
- package/dist/chunk-7UY2LDRW.js.map +1 -0
- package/dist/chunk-S4Z7A4TW.js +116 -0
- package/dist/chunk-S4Z7A4TW.js.map +1 -0
- package/dist/{chunk-ZS5MZR75.js → chunk-UNVCJMJK.js} +4 -114
- package/dist/chunk-UNVCJMJK.js.map +1 -0
- package/dist/client/index.d.cts +4 -3
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/{createDevToolsServer-B7NQ_5H4.d.cts → createDevToolsServer-Cqb7xzIv.d.cts} +1 -1
- package/dist/{createDevToolsServer-DlNMoCJR.d.ts → createDevToolsServer-D3QnP8Yb.d.ts} +1 -1
- package/dist/{handleDeviceSnapshot-cYGuitSc.d.cts → handleDeviceSnapshot-CIZ_00fE.d.cts} +2 -50
- package/dist/{handleDeviceSnapshot-CR-4QMdk.d.ts → handleDeviceSnapshot-DF9sS1Pm.d.ts} +2 -50
- package/dist/index.d.cts +13 -45
- package/dist/index.d.ts +13 -45
- package/dist/index.js +8 -2741
- package/dist/index.js.map +1 -1
- package/dist/inspector/index.d.cts +2 -2
- package/dist/inspector/index.d.ts +2 -2
- package/dist/inspectors/sqlite/index.d.cts +2 -2
- package/dist/inspectors/sqlite/index.d.ts +2 -2
- package/dist/native.cjs +3946 -0
- package/dist/native.cjs.map +1 -0
- package/dist/native.d.cts +37 -0
- package/dist/native.d.ts +37 -0
- package/dist/native.js +13 -0
- package/dist/native.js.map +1 -0
- package/dist/{register-BuicFu9L.d.cts → register-DEZ0kK1p.d.cts} +2 -2
- package/dist/{register-aITLN9Q9.d.ts → register-Dawue1_L.d.ts} +2 -2
- package/dist/resolveDeviceDatabaseUrl-DhRP-h1Y.d.cts +49 -0
- package/dist/resolveDeviceDatabaseUrl-DhRP-h1Y.d.ts +49 -0
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/{snapshot-Dhyecgo2.d.cts → snapshot-Dknp_qP1.d.cts} +1 -1
- package/dist/{snapshot-Dhyecgo2.d.ts → snapshot-Dknp_qP1.d.ts} +1 -1
- package/dist/{types-CKVRCX-e.d.ts → types-6nco_RRp.d.ts} +2 -2
- package/dist/{types-CIA11diJ.d.cts → types-BQHZtFwx.d.cts} +2 -2
- package/dist/{types-CF6qEU-_.d.ts → types-DkVlU6t-.d.ts} +2 -2
- package/dist/{types-BRLJdBRY.d.cts → types-gT3gDw_T.d.cts} +2 -2
- package/dist/web/assets/{index-DQrPRTOy.js → index-DogMAkgu.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +5 -5
- package/dist/chunk-ZS5MZR75.js.map +0 -1
package/dist/native.cjs
ADDED
|
@@ -0,0 +1,3946 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var reactNative = require('react-native');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var MaterialCommunityIcons = require('@expo/vector-icons/MaterialCommunityIcons');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var MaterialCommunityIcons__default = /*#__PURE__*/_interopDefault(MaterialCommunityIcons);
|
|
11
|
+
|
|
12
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
13
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
14
|
+
}) : x)(function(x) {
|
|
15
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
16
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// src/types/protocol.ts
|
|
20
|
+
var DevToolsRole = {
|
|
21
|
+
MOBILE: "mobile",
|
|
22
|
+
BROWSER: "browser"
|
|
23
|
+
};
|
|
24
|
+
var MessageType = {
|
|
25
|
+
REGISTER: "register",
|
|
26
|
+
PING: "ping",
|
|
27
|
+
PONG: "pong",
|
|
28
|
+
DEVICE_STATUS: "deviceStatus",
|
|
29
|
+
BROADCAST: "broadcast",
|
|
30
|
+
REFRESH_REQUEST: "refreshRequest",
|
|
31
|
+
SNAPSHOT_UPLOAD_REQUESTED: "snapshotUploadRequested",
|
|
32
|
+
REFRESH_STATUS: "refreshStatus",
|
|
33
|
+
SNAPSHOT_READY: "snapshotReady",
|
|
34
|
+
REFRESH_ERROR: "refreshError",
|
|
35
|
+
EXPORT_SNAPSHOT_REQUEST: "exportSnapshotRequest",
|
|
36
|
+
EXPORT_SNAPSHOT_ERROR: "exportSnapshotError",
|
|
37
|
+
BEGIN_TRANSACTION_REQUEST: "beginTransactionRequest",
|
|
38
|
+
COMMIT_TRANSACTION_REQUEST: "commitTransactionRequest",
|
|
39
|
+
ROLLBACK_TRANSACTION_REQUEST: "rollbackTransactionRequest",
|
|
40
|
+
WRITE_REQUEST: "writeRequest",
|
|
41
|
+
BEGIN_TRANSACTION: "beginTransaction",
|
|
42
|
+
COMMIT_TRANSACTION: "commitTransaction",
|
|
43
|
+
ROLLBACK_TRANSACTION: "rollbackTransaction",
|
|
44
|
+
EXECUTE_WRITE: "executeWrite",
|
|
45
|
+
TRANSACTION_ACK: "transactionAck",
|
|
46
|
+
WRITE_ACK: "writeAck",
|
|
47
|
+
TRANSACTION_STATUS: "transactionStatus",
|
|
48
|
+
WRITE_RESULT: "writeResult",
|
|
49
|
+
WRITE_ERROR: "writeError"
|
|
50
|
+
};
|
|
51
|
+
var DEVTOOLS_WS_PATH = "/ws";
|
|
52
|
+
var DEFAULT_DEVTOOLS_PORT = 3847;
|
|
53
|
+
var DEFAULT_DEVTOOLS_HOST = "0.0.0.0";
|
|
54
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
55
|
+
var HEARTBEAT_TIMEOUT_MS = 1e4;
|
|
56
|
+
var DEVICE_SNAPSHOT_API_PATH = "/api/devices";
|
|
57
|
+
var SNAPSHOT_API_PATH = DEVICE_SNAPSHOT_API_PATH;
|
|
58
|
+
var REFRESH_TIMEOUT_MS = 6e4;
|
|
59
|
+
var SYNC_TIMEOUT_MS = REFRESH_TIMEOUT_MS;
|
|
60
|
+
var MAX_SNAPSHOT_BYTES = 50 * 1024 * 1024;
|
|
61
|
+
function createMessage(message) {
|
|
62
|
+
return { ...message, timestamp: Date.now() };
|
|
63
|
+
}
|
|
64
|
+
function buildDevToolsHttpUrl(host = "localhost", port = DEFAULT_DEVTOOLS_PORT) {
|
|
65
|
+
return `http://${host}:${port}`;
|
|
66
|
+
}
|
|
67
|
+
function buildDevToolsWsUrl(host = "localhost", port = DEFAULT_DEVTOOLS_PORT) {
|
|
68
|
+
return `ws://${host}:${port}${DEVTOOLS_WS_PATH}`;
|
|
69
|
+
}
|
|
70
|
+
function buildDeviceSnapshotUrl(httpBaseUrl, deviceId) {
|
|
71
|
+
const base = httpBaseUrl.replace(/\/$/, "");
|
|
72
|
+
return `${base}${DEVICE_SNAPSHOT_API_PATH}/${encodeURIComponent(deviceId)}/snapshot`;
|
|
73
|
+
}
|
|
74
|
+
function buildSnapshotUrl(httpBaseUrl, deviceId) {
|
|
75
|
+
return buildDeviceSnapshotUrl(httpBaseUrl, deviceId);
|
|
76
|
+
}
|
|
77
|
+
function wsUrlToHttpUrl(wsUrl) {
|
|
78
|
+
const url = new URL(wsUrl);
|
|
79
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
80
|
+
url.pathname = "";
|
|
81
|
+
url.search = "";
|
|
82
|
+
url.hash = "";
|
|
83
|
+
return url.toString().replace(/\/$/, "");
|
|
84
|
+
}
|
|
85
|
+
function hasMessageType(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
87
|
+
}
|
|
88
|
+
function isRegisterMessage(value) {
|
|
89
|
+
if (!hasMessageType(value) || value.type !== MessageType.REGISTER) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const message = value;
|
|
93
|
+
return message.role === DevToolsRole.MOBILE || message.role === DevToolsRole.BROWSER;
|
|
94
|
+
}
|
|
95
|
+
function isPongMessage(value) {
|
|
96
|
+
if (!hasMessageType(value) || value.type !== MessageType.PONG) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const message = value;
|
|
100
|
+
return typeof message.pingId === "string";
|
|
101
|
+
}
|
|
102
|
+
function isRefreshRequestMessage(value) {
|
|
103
|
+
if (!hasMessageType(value) || value.type !== MessageType.REFRESH_REQUEST) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const message = value;
|
|
107
|
+
return typeof message.deviceId === "string" && message.refreshType === "snapshot";
|
|
108
|
+
}
|
|
109
|
+
function isSnapshotUploadRequestedMessage(value) {
|
|
110
|
+
if (!hasMessageType(value) || value.type !== MessageType.SNAPSHOT_UPLOAD_REQUESTED) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const message = value;
|
|
114
|
+
return typeof message.deviceId === "string" && message.refreshType === "snapshot";
|
|
115
|
+
}
|
|
116
|
+
function isRefreshStatusMessage(value) {
|
|
117
|
+
if (!hasMessageType(value) || value.type !== MessageType.REFRESH_STATUS) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const message = value;
|
|
121
|
+
return typeof message.deviceId === "string" && typeof message.state === "string";
|
|
122
|
+
}
|
|
123
|
+
function isSnapshotReadyMessage(value) {
|
|
124
|
+
if (!hasMessageType(value) || value.type !== MessageType.SNAPSHOT_READY) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const message = value;
|
|
128
|
+
return typeof message.deviceId === "string" && typeof message.size === "number" && typeof message.exportedAt === "number" && typeof message.kind === "string" && typeof message.mimeType === "string";
|
|
129
|
+
}
|
|
130
|
+
function isRefreshErrorMessage(value) {
|
|
131
|
+
if (!hasMessageType(value) || value.type !== MessageType.REFRESH_ERROR) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const message = value;
|
|
135
|
+
return typeof message.deviceId === "string" && typeof message.code === "string";
|
|
136
|
+
}
|
|
137
|
+
function isExportSnapshotRequestMessage(value) {
|
|
138
|
+
if (!hasMessageType(value) || value.type !== MessageType.EXPORT_SNAPSHOT_REQUEST) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const message = value;
|
|
142
|
+
return message.refreshType === "snapshot";
|
|
143
|
+
}
|
|
144
|
+
function isExportSnapshotErrorMessage(value) {
|
|
145
|
+
if (!hasMessageType(value) || value.type !== MessageType.EXPORT_SNAPSHOT_ERROR) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const message = value;
|
|
149
|
+
return typeof message.code === "string" && typeof message.message === "string";
|
|
150
|
+
}
|
|
151
|
+
function isBeginTransactionRequestMessage(value) {
|
|
152
|
+
if (!hasMessageType(value) || value.type !== MessageType.BEGIN_TRANSACTION_REQUEST) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const message = value;
|
|
156
|
+
return typeof message.transactionId === "string" && typeof message.deviceId === "string";
|
|
157
|
+
}
|
|
158
|
+
function isCommitTransactionRequestMessage(value) {
|
|
159
|
+
if (!hasMessageType(value) || value.type !== MessageType.COMMIT_TRANSACTION_REQUEST) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const message = value;
|
|
163
|
+
return typeof message.transactionId === "string" && typeof message.deviceId === "string";
|
|
164
|
+
}
|
|
165
|
+
function isRollbackTransactionRequestMessage(value) {
|
|
166
|
+
if (!hasMessageType(value) || value.type !== MessageType.ROLLBACK_TRANSACTION_REQUEST) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const message = value;
|
|
170
|
+
return typeof message.transactionId === "string" && typeof message.deviceId === "string";
|
|
171
|
+
}
|
|
172
|
+
function isWriteRequestMessage(value) {
|
|
173
|
+
if (!hasMessageType(value) || value.type !== MessageType.WRITE_REQUEST) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const message = value;
|
|
177
|
+
return typeof message.writeId === "string" && typeof message.transactionId === "string" && typeof message.deviceId === "string" && typeof message.operation === "object" && message.operation !== null;
|
|
178
|
+
}
|
|
179
|
+
function isTransactionAckMessage(value) {
|
|
180
|
+
if (!hasMessageType(value) || value.type !== MessageType.TRANSACTION_ACK) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const message = value;
|
|
184
|
+
return typeof message.transactionId === "string" && (message.action === "begin" || message.action === "commit" || message.action === "rollback") && typeof message.ok === "boolean";
|
|
185
|
+
}
|
|
186
|
+
function isWriteAckMessage(value) {
|
|
187
|
+
if (!hasMessageType(value) || value.type !== MessageType.WRITE_ACK) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const message = value;
|
|
191
|
+
return typeof message.writeId === "string" && typeof message.transactionId === "string" && typeof message.ok === "boolean";
|
|
192
|
+
}
|
|
193
|
+
function isClientMessage(value) {
|
|
194
|
+
return isRegisterMessage(value) || isPongMessage(value) || isRefreshRequestMessage(value) || isExportSnapshotRequestMessage(value) || isBeginTransactionRequestMessage(value) || isCommitTransactionRequestMessage(value) || isRollbackTransactionRequestMessage(value) || isWriteRequestMessage(value) || isTransactionAckMessage(value) || isWriteAckMessage(value);
|
|
195
|
+
}
|
|
196
|
+
function isPingMessage(value) {
|
|
197
|
+
if (!hasMessageType(value) || value.type !== MessageType.PING) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const message = value;
|
|
201
|
+
return typeof message.pingId === "string";
|
|
202
|
+
}
|
|
203
|
+
function isDeviceStatusMessage(value) {
|
|
204
|
+
if (!hasMessageType(value) || value.type !== MessageType.DEVICE_STATUS) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const message = value;
|
|
208
|
+
return typeof message.payload === "object" && message.payload !== null;
|
|
209
|
+
}
|
|
210
|
+
function isBroadcastMessage(value) {
|
|
211
|
+
if (!hasMessageType(value) || value.type !== MessageType.BROADCAST) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
function isBeginTransactionMessage(value) {
|
|
217
|
+
if (!hasMessageType(value) || value.type !== MessageType.BEGIN_TRANSACTION) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const message = value;
|
|
221
|
+
return typeof message.transactionId === "string";
|
|
222
|
+
}
|
|
223
|
+
function isCommitTransactionMessage(value) {
|
|
224
|
+
if (!hasMessageType(value) || value.type !== MessageType.COMMIT_TRANSACTION) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const message = value;
|
|
228
|
+
return typeof message.transactionId === "string";
|
|
229
|
+
}
|
|
230
|
+
function isRollbackTransactionMessage(value) {
|
|
231
|
+
if (!hasMessageType(value) || value.type !== MessageType.ROLLBACK_TRANSACTION) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const message = value;
|
|
235
|
+
return typeof message.transactionId === "string";
|
|
236
|
+
}
|
|
237
|
+
function isExecuteWriteMessage(value) {
|
|
238
|
+
if (!hasMessageType(value) || value.type !== MessageType.EXECUTE_WRITE) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const message = value;
|
|
242
|
+
return typeof message.writeId === "string" && typeof message.transactionId === "string" && typeof message.operation === "object" && message.operation !== null;
|
|
243
|
+
}
|
|
244
|
+
function isTransactionStatusMessage(value) {
|
|
245
|
+
if (!hasMessageType(value) || value.type !== MessageType.TRANSACTION_STATUS) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const message = value;
|
|
249
|
+
return typeof message.transactionId === "string" && typeof message.deviceId === "string" && typeof message.state === "string";
|
|
250
|
+
}
|
|
251
|
+
function isWriteResultMessage(value) {
|
|
252
|
+
if (!hasMessageType(value) || value.type !== MessageType.WRITE_RESULT) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const message = value;
|
|
256
|
+
return typeof message.writeId === "string" && typeof message.transactionId === "string" && typeof message.rowsAffected === "number";
|
|
257
|
+
}
|
|
258
|
+
function isWriteErrorMessage(value) {
|
|
259
|
+
if (!hasMessageType(value) || value.type !== MessageType.WRITE_ERROR) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const message = value;
|
|
263
|
+
return typeof message.transactionId === "string" && typeof message.code === "string";
|
|
264
|
+
}
|
|
265
|
+
function isServerMessage(value) {
|
|
266
|
+
return isPingMessage(value) || isDeviceStatusMessage(value) || isBroadcastMessage(value) || isSnapshotUploadRequestedMessage(value) || isRefreshStatusMessage(value) || isSnapshotReadyMessage(value) || isRefreshErrorMessage(value) || isExportSnapshotErrorMessage(value) || isBeginTransactionMessage(value) || isCommitTransactionMessage(value) || isRollbackTransactionMessage(value) || isExecuteWriteMessage(value) || isTransactionStatusMessage(value) || isWriteResultMessage(value) || isWriteErrorMessage(value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/types/adapter.ts
|
|
270
|
+
function isWritableDatabaseAdapter(adapter) {
|
|
271
|
+
if (!adapter?.capabilities.transactionalWrites) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const candidate = adapter;
|
|
275
|
+
return typeof candidate.beginTransaction === "function" && typeof candidate.commitTransaction === "function" && typeof candidate.rollbackTransaction === "function" && typeof candidate.executeWrite === "function";
|
|
276
|
+
}
|
|
277
|
+
var isEditableDatabaseAdapter = isWritableDatabaseAdapter;
|
|
278
|
+
|
|
279
|
+
// src/types/kind.ts
|
|
280
|
+
var DATABASE_DEVTOOLS_KIND = /* @__PURE__ */ Symbol.for("@database-devtools/kind");
|
|
281
|
+
function readDatabaseKindMarker(value) {
|
|
282
|
+
if (typeof value !== "object" || value === null) {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
const marker = value[DATABASE_DEVTOOLS_KIND];
|
|
286
|
+
return typeof marker === "string" ? marker : void 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/types/snapshot.ts
|
|
290
|
+
var SNAPSHOT_KIND_HEADER = "x-database-kind";
|
|
291
|
+
var SNAPSHOT_MIME_HEADER = "x-snapshot-mime-type";
|
|
292
|
+
var SNAPSHOT_NAME_HEADER = "x-database-name";
|
|
293
|
+
var SQLITE_SNAPSHOT_MIME_TYPE = "application/x-sqlite3";
|
|
294
|
+
|
|
295
|
+
// src/adapter/errors.ts
|
|
296
|
+
var AdapterResolutionError = class extends Error {
|
|
297
|
+
constructor(supported, hint) {
|
|
298
|
+
super(formatAdapterResolutionMessage(supported, hint));
|
|
299
|
+
this.supported = supported;
|
|
300
|
+
this.hint = hint;
|
|
301
|
+
}
|
|
302
|
+
supported;
|
|
303
|
+
hint;
|
|
304
|
+
name = "AdapterResolutionError";
|
|
305
|
+
};
|
|
306
|
+
function formatAdapterResolutionMessage(supported, hint) {
|
|
307
|
+
const lines = [
|
|
308
|
+
"Unable to determine the database type.",
|
|
309
|
+
"",
|
|
310
|
+
"Supported databases:",
|
|
311
|
+
...supported.map((entry) => ` \u2022 ${entry.displayName}`),
|
|
312
|
+
"",
|
|
313
|
+
"Try specifying a type:",
|
|
314
|
+
' <DatabaseDevTools database={db} type="sqlite" />',
|
|
315
|
+
"",
|
|
316
|
+
"Or provide a custom adapter:",
|
|
317
|
+
" <DatabaseDevTools database={db} adapter={myAdapter} />"
|
|
318
|
+
];
|
|
319
|
+
if (hint === "install-package") {
|
|
320
|
+
lines.push(
|
|
321
|
+
"",
|
|
322
|
+
"For SQLite, install expo-sqlite:",
|
|
323
|
+
" npm install expo-sqlite"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/adapter/registry.ts
|
|
330
|
+
var AdapterRegistry = class {
|
|
331
|
+
definitions = /* @__PURE__ */ new Map();
|
|
332
|
+
register(definition) {
|
|
333
|
+
this.definitions.set(definition.kind, definition);
|
|
334
|
+
}
|
|
335
|
+
get(kind) {
|
|
336
|
+
return this.definitions.get(kind);
|
|
337
|
+
}
|
|
338
|
+
listSupported() {
|
|
339
|
+
return [...this.definitions.values()].sort((left, right) => left.displayName.localeCompare(right.displayName)).map((definition) => ({
|
|
340
|
+
kind: definition.kind,
|
|
341
|
+
displayName: definition.displayName
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
detect(database) {
|
|
345
|
+
const markerKind = readDatabaseKindMarker(database);
|
|
346
|
+
if (markerKind) {
|
|
347
|
+
const marked = this.definitions.get(markerKind);
|
|
348
|
+
if (marked) {
|
|
349
|
+
return marked;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const matches = [...this.definitions.values()].filter((definition) => {
|
|
353
|
+
try {
|
|
354
|
+
return definition.detect(database);
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}).sort((left, right) => right.priority - left.priority);
|
|
359
|
+
return matches[0];
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var globalRegistry = null;
|
|
363
|
+
function getAdapterRegistry() {
|
|
364
|
+
if (!globalRegistry) {
|
|
365
|
+
globalRegistry = new AdapterRegistry();
|
|
366
|
+
}
|
|
367
|
+
return globalRegistry;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/adapters/sqlite/buildWriteSql.ts
|
|
371
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
372
|
+
function quoteIdentifier(name) {
|
|
373
|
+
if (!IDENTIFIER_PATTERN.test(name)) {
|
|
374
|
+
throw new Error(`Invalid identifier: ${name}`);
|
|
375
|
+
}
|
|
376
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
377
|
+
}
|
|
378
|
+
function buildInsertSql(operation) {
|
|
379
|
+
const columns = Object.keys(operation.values);
|
|
380
|
+
if (columns.length === 0) {
|
|
381
|
+
throw new Error("Insert requires at least one column value");
|
|
382
|
+
}
|
|
383
|
+
const quotedColumns = columns.map(quoteIdentifier).join(", ");
|
|
384
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
385
|
+
const sql = `INSERT INTO ${quoteIdentifier(operation.table)} (${quotedColumns}) VALUES (${placeholders})`;
|
|
386
|
+
const params = columns.map((column) => operation.values[column] ?? null);
|
|
387
|
+
return { sql, params };
|
|
388
|
+
}
|
|
389
|
+
function buildUpdateSql(operation) {
|
|
390
|
+
const valueColumns = Object.keys(operation.values);
|
|
391
|
+
const pkColumns = Object.keys(operation.primaryKey);
|
|
392
|
+
if (valueColumns.length === 0) {
|
|
393
|
+
throw new Error("Update requires at least one column value");
|
|
394
|
+
}
|
|
395
|
+
if (pkColumns.length === 0) {
|
|
396
|
+
throw new Error("Update requires a primary key");
|
|
397
|
+
}
|
|
398
|
+
const setClause = valueColumns.map((column) => `${quoteIdentifier(column)} = ?`).join(", ");
|
|
399
|
+
const whereClause = pkColumns.map((column) => `${quoteIdentifier(column)} = ?`).join(" AND ");
|
|
400
|
+
const sql = `UPDATE ${quoteIdentifier(operation.table)} SET ${setClause} WHERE ${whereClause}`;
|
|
401
|
+
const params = [
|
|
402
|
+
...valueColumns.map((column) => operation.values[column] ?? null),
|
|
403
|
+
...pkColumns.map((column) => operation.primaryKey[column] ?? null)
|
|
404
|
+
];
|
|
405
|
+
return { sql, params };
|
|
406
|
+
}
|
|
407
|
+
function buildDeleteSql(operation) {
|
|
408
|
+
const pkColumns = Object.keys(operation.primaryKey);
|
|
409
|
+
if (pkColumns.length === 0) {
|
|
410
|
+
throw new Error("Delete requires a primary key");
|
|
411
|
+
}
|
|
412
|
+
const whereClause = pkColumns.map((column) => `${quoteIdentifier(column)} = ?`).join(" AND ");
|
|
413
|
+
const sql = `DELETE FROM ${quoteIdentifier(operation.table)} WHERE ${whereClause}`;
|
|
414
|
+
const params = pkColumns.map((column) => operation.primaryKey[column] ?? null);
|
|
415
|
+
return { sql, params };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/adapters/sqlite/createExpoSqliteAdapter.ts
|
|
419
|
+
function createExpoSqliteAdapter(options) {
|
|
420
|
+
const { database } = options;
|
|
421
|
+
let inTransaction = false;
|
|
422
|
+
let operationQueue = Promise.resolve();
|
|
423
|
+
const enqueue = async (operation) => {
|
|
424
|
+
const next = operationQueue.then(operation);
|
|
425
|
+
operationQueue = next.then(
|
|
426
|
+
() => void 0,
|
|
427
|
+
() => void 0
|
|
428
|
+
);
|
|
429
|
+
return next;
|
|
430
|
+
};
|
|
431
|
+
const assertTransaction = () => {
|
|
432
|
+
if (!inTransaction) {
|
|
433
|
+
throw new Error("No open transaction. Call beginTransaction() first.");
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const runWrite = async (operation) => {
|
|
437
|
+
assertTransaction();
|
|
438
|
+
let sql;
|
|
439
|
+
let params;
|
|
440
|
+
switch (operation.kind) {
|
|
441
|
+
case "insert": {
|
|
442
|
+
const built = buildInsertSql(operation);
|
|
443
|
+
sql = built.sql;
|
|
444
|
+
params = built.params;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case "update": {
|
|
448
|
+
const built = buildUpdateSql(operation);
|
|
449
|
+
sql = built.sql;
|
|
450
|
+
params = built.params;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
case "delete": {
|
|
454
|
+
const built = buildDeleteSql(operation);
|
|
455
|
+
sql = built.sql;
|
|
456
|
+
params = built.params;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
default: {
|
|
460
|
+
const exhaustive = operation;
|
|
461
|
+
throw new Error(`Unsupported write operation: ${exhaustive.kind}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const result = await database.runAsync(sql, params);
|
|
465
|
+
return { rowsAffected: result.changes };
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
kind: "sqlite",
|
|
469
|
+
dialect: "sqlite",
|
|
470
|
+
id: options.id ?? database.databasePath,
|
|
471
|
+
name: options.name ?? "SQLite",
|
|
472
|
+
capabilities: {
|
|
473
|
+
exportSnapshot: true,
|
|
474
|
+
executeQuery: false,
|
|
475
|
+
listTables: false,
|
|
476
|
+
getSchema: false,
|
|
477
|
+
transactionalWrites: true,
|
|
478
|
+
observeChanges: false,
|
|
479
|
+
importSnapshot: false
|
|
480
|
+
},
|
|
481
|
+
async exportSnapshot() {
|
|
482
|
+
return enqueue(async () => {
|
|
483
|
+
if (inTransaction) {
|
|
484
|
+
throw new Error("Cannot export while a write transaction is open");
|
|
485
|
+
}
|
|
486
|
+
await database.execAsync("PRAGMA wal_checkpoint(FULL)");
|
|
487
|
+
const bytes = await database.serializeAsync();
|
|
488
|
+
return {
|
|
489
|
+
bytes,
|
|
490
|
+
mimeType: SQLITE_SNAPSHOT_MIME_TYPE,
|
|
491
|
+
kind: "sqlite",
|
|
492
|
+
exportedAt: Date.now()
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
async beginTransaction() {
|
|
497
|
+
return enqueue(async () => {
|
|
498
|
+
if (inTransaction) {
|
|
499
|
+
throw new Error("Transaction already open");
|
|
500
|
+
}
|
|
501
|
+
await database.execAsync("BEGIN IMMEDIATE");
|
|
502
|
+
inTransaction = true;
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
async commitTransaction() {
|
|
506
|
+
return enqueue(async () => {
|
|
507
|
+
assertTransaction();
|
|
508
|
+
try {
|
|
509
|
+
await database.execAsync("COMMIT");
|
|
510
|
+
} finally {
|
|
511
|
+
inTransaction = false;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
async rollbackTransaction() {
|
|
516
|
+
return enqueue(async () => {
|
|
517
|
+
if (!inTransaction) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await database.execAsync("ROLLBACK");
|
|
522
|
+
} finally {
|
|
523
|
+
inTransaction = false;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
executeWrite: (operation) => enqueue(() => runWrite(operation))
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/adapters/sqlite/detect.ts
|
|
532
|
+
function detectExpoSqlite(database) {
|
|
533
|
+
return typeof database === "object" && database !== null && typeof database.execAsync === "function" && typeof database.serializeAsync === "function" && typeof database.runAsync === "function" && typeof database.databasePath === "string";
|
|
534
|
+
}
|
|
535
|
+
function detectExpoSqliteInspectable(database) {
|
|
536
|
+
return detectExpoSqlite(database) && typeof database.getAllAsync === "function" && typeof database.getFirstAsync === "function";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/adapters/sqlite/register.ts
|
|
540
|
+
var sqliteAdapterDefinition = {
|
|
541
|
+
kind: "sqlite",
|
|
542
|
+
displayName: "SQLite",
|
|
543
|
+
priority: 100,
|
|
544
|
+
detect: detectExpoSqlite,
|
|
545
|
+
create: (database) => createExpoSqliteAdapter({
|
|
546
|
+
database,
|
|
547
|
+
name: database.databasePath
|
|
548
|
+
})
|
|
549
|
+
};
|
|
550
|
+
function registerSqliteAdapter() {
|
|
551
|
+
getAdapterRegistry().register(sqliteAdapterDefinition);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/adapter/registerBuiltins.ts
|
|
555
|
+
async function registerBuiltInAdapters() {
|
|
556
|
+
registerSqliteAdapter();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/adapter/resolveAdapter.ts
|
|
560
|
+
async function resolveAdapter(database, options = {}) {
|
|
561
|
+
if (options.adapter) {
|
|
562
|
+
return options.adapter;
|
|
563
|
+
}
|
|
564
|
+
if (!database) {
|
|
565
|
+
throw new AdapterResolutionError([], "adapter");
|
|
566
|
+
}
|
|
567
|
+
await registerBuiltInAdapters();
|
|
568
|
+
const registry = getAdapterRegistry();
|
|
569
|
+
if (options.type) {
|
|
570
|
+
const definition = registry.get(options.type);
|
|
571
|
+
if (!definition) {
|
|
572
|
+
throw new AdapterResolutionError(registry.listSupported(), "type");
|
|
573
|
+
}
|
|
574
|
+
return definition.create(database);
|
|
575
|
+
}
|
|
576
|
+
const detected = registry.detect(database);
|
|
577
|
+
if (!detected) {
|
|
578
|
+
throw new AdapterResolutionError(
|
|
579
|
+
registry.listSupported(),
|
|
580
|
+
registry.listSupported().length === 0 ? "install-package" : "type"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return detected.create(database);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/inspector/registry.ts
|
|
587
|
+
var InspectorRegistry = class {
|
|
588
|
+
definitions = /* @__PURE__ */ new Map();
|
|
589
|
+
register(definition) {
|
|
590
|
+
this.definitions.set(definition.kind, definition);
|
|
591
|
+
}
|
|
592
|
+
get(kind) {
|
|
593
|
+
return this.definitions.get(kind);
|
|
594
|
+
}
|
|
595
|
+
listSupported() {
|
|
596
|
+
return [...this.definitions.values()].sort((left, right) => left.displayName.localeCompare(right.displayName)).map((definition) => ({
|
|
597
|
+
kind: definition.kind,
|
|
598
|
+
displayName: definition.displayName
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
detectFromBytes(bytes) {
|
|
602
|
+
return [...this.definitions.values()].find((definition) => definition.canOpenBytes?.(bytes));
|
|
603
|
+
}
|
|
604
|
+
detectFromSnapshot(input) {
|
|
605
|
+
const byKind = this.definitions.get(input.kind);
|
|
606
|
+
if (byKind) {
|
|
607
|
+
return byKind;
|
|
608
|
+
}
|
|
609
|
+
return [...this.definitions.values()].find(
|
|
610
|
+
(definition) => definition.mimeTypes.includes(input.mimeType)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
var globalInspectorRegistry = null;
|
|
615
|
+
function getInspectorRegistry() {
|
|
616
|
+
if (!globalInspectorRegistry) {
|
|
617
|
+
globalInspectorRegistry = new InspectorRegistry();
|
|
618
|
+
}
|
|
619
|
+
return globalInspectorRegistry;
|
|
620
|
+
}
|
|
621
|
+
async function createInspectorForSnapshot(input) {
|
|
622
|
+
const registry = getInspectorRegistry();
|
|
623
|
+
const definition = registry.get(input.kind) ?? registry.detectFromBytes(input.bytes) ?? registry.detectFromSnapshot(input);
|
|
624
|
+
if (!definition) {
|
|
625
|
+
throw new Error(`No inspector registered for database kind "${input.kind}"`);
|
|
626
|
+
}
|
|
627
|
+
const inspector = await definition.createInspector();
|
|
628
|
+
await inspector.open(input.bytes);
|
|
629
|
+
return inspector;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/utils/ids.ts
|
|
633
|
+
function generateDeviceId() {
|
|
634
|
+
return generateUniqueId("device");
|
|
635
|
+
}
|
|
636
|
+
function generateTransactionId() {
|
|
637
|
+
return generateUniqueId("txn");
|
|
638
|
+
}
|
|
639
|
+
function generateWriteId() {
|
|
640
|
+
return generateUniqueId("write");
|
|
641
|
+
}
|
|
642
|
+
function generateUniqueId(prefix) {
|
|
643
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
644
|
+
return `${prefix}-${crypto.randomUUID()}`;
|
|
645
|
+
}
|
|
646
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
647
|
+
return `${prefix}-${Date.now()}-${random}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/utils/reconnect.ts
|
|
651
|
+
var DEFAULT_BASE_MS = 1e3;
|
|
652
|
+
var DEFAULT_MAX_MS = 3e4;
|
|
653
|
+
var DEFAULT_JITTER_MS = 500;
|
|
654
|
+
function calculateBackoffDelay(attempt, options = {}) {
|
|
655
|
+
const baseMs = options.baseMs ?? DEFAULT_BASE_MS;
|
|
656
|
+
const maxMs = options.maxMs ?? DEFAULT_MAX_MS;
|
|
657
|
+
const jitterMs = options.jitterMs ?? DEFAULT_JITTER_MS;
|
|
658
|
+
const exponential = baseMs * 2 ** attempt;
|
|
659
|
+
const capped = Math.min(exponential, maxMs);
|
|
660
|
+
const jitter = Math.floor(Math.random() * jitterMs);
|
|
661
|
+
return capped + jitter;
|
|
662
|
+
}
|
|
663
|
+
var ReconnectScheduler = class {
|
|
664
|
+
attempt = 0;
|
|
665
|
+
timer = null;
|
|
666
|
+
onReconnect;
|
|
667
|
+
options;
|
|
668
|
+
constructor({ onReconnect, ...options }) {
|
|
669
|
+
this.onReconnect = onReconnect;
|
|
670
|
+
this.options = options;
|
|
671
|
+
}
|
|
672
|
+
schedule() {
|
|
673
|
+
if (this.timer !== null) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const delay = calculateBackoffDelay(this.attempt, this.options);
|
|
677
|
+
this.attempt += 1;
|
|
678
|
+
this.timer = setTimeout(() => {
|
|
679
|
+
this.timer = null;
|
|
680
|
+
this.onReconnect();
|
|
681
|
+
}, delay);
|
|
682
|
+
}
|
|
683
|
+
reset() {
|
|
684
|
+
this.attempt = 0;
|
|
685
|
+
this.cancel();
|
|
686
|
+
}
|
|
687
|
+
cancel() {
|
|
688
|
+
if (this.timer !== null) {
|
|
689
|
+
clearTimeout(this.timer);
|
|
690
|
+
this.timer = null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/client/createDevToolsClient.ts
|
|
696
|
+
var WRITE_TIMEOUT_MS = 3e4;
|
|
697
|
+
function createDevToolsClient(options = {}) {
|
|
698
|
+
const role = options.role ?? DevToolsRole.MOBILE;
|
|
699
|
+
const WebSocketImpl = options.WebSocketImpl ?? globalThis.WebSocket;
|
|
700
|
+
if (!WebSocketImpl) {
|
|
701
|
+
throw new Error("WebSocket is not available in this environment.");
|
|
702
|
+
}
|
|
703
|
+
let socket = null;
|
|
704
|
+
let deviceStatus = null;
|
|
705
|
+
let connectionState = "disconnected";
|
|
706
|
+
let lastConnectionError = null;
|
|
707
|
+
let intentionalDisconnect = false;
|
|
708
|
+
let transactionState = "idle";
|
|
709
|
+
let activeTransactionId = null;
|
|
710
|
+
let activeDeviceId = null;
|
|
711
|
+
const pendingTransaction = /* @__PURE__ */ new Map();
|
|
712
|
+
const pendingWrites = /* @__PURE__ */ new Map();
|
|
713
|
+
const deviceId = role === DevToolsRole.MOBILE ? options.deviceId ?? generateDeviceId() : void 0;
|
|
714
|
+
let wsUrl = options.serverUrl ?? buildDevToolsWsUrl("localhost", DEFAULT_DEVTOOLS_PORT);
|
|
715
|
+
const setConnectionState = (state) => {
|
|
716
|
+
if (connectionState === state) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
connectionState = state;
|
|
720
|
+
options.onConnectionStateChange?.(state);
|
|
721
|
+
};
|
|
722
|
+
const setTransactionState = (state) => {
|
|
723
|
+
if (transactionState === state) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
transactionState = state;
|
|
727
|
+
options.onTransactionStateChange?.(state);
|
|
728
|
+
};
|
|
729
|
+
const resetTransaction = () => {
|
|
730
|
+
setTransactionState("idle");
|
|
731
|
+
activeTransactionId = null;
|
|
732
|
+
activeDeviceId = null;
|
|
733
|
+
};
|
|
734
|
+
const rejectPendingTransaction = (transactionId, error) => {
|
|
735
|
+
const pending = pendingTransaction.get(transactionId);
|
|
736
|
+
if (pending) {
|
|
737
|
+
pending.reject(error);
|
|
738
|
+
pendingTransaction.delete(transactionId);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const rejectAllPending = (error) => {
|
|
742
|
+
for (const [id, pending] of pendingTransaction) {
|
|
743
|
+
pending.reject(error);
|
|
744
|
+
pendingTransaction.delete(id);
|
|
745
|
+
}
|
|
746
|
+
for (const [id, pending] of pendingWrites) {
|
|
747
|
+
pending.reject(error);
|
|
748
|
+
pendingWrites.delete(id);
|
|
749
|
+
}
|
|
750
|
+
resetTransaction();
|
|
751
|
+
};
|
|
752
|
+
const reconnectScheduler = new ReconnectScheduler({
|
|
753
|
+
onReconnect: () => {
|
|
754
|
+
openSocket();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
const send = (message) => {
|
|
758
|
+
if (!socket || socket.readyState !== WebSocketImpl.OPEN) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
socket.send(JSON.stringify({ ...message, timestamp: Date.now() }));
|
|
762
|
+
};
|
|
763
|
+
const sendRegister = () => {
|
|
764
|
+
send({
|
|
765
|
+
type: MessageType.REGISTER,
|
|
766
|
+
role,
|
|
767
|
+
...deviceId ? { deviceId } : {},
|
|
768
|
+
...options.metadata ? { metadata: options.metadata } : {}
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
const handleServerMessage = (parsed) => {
|
|
772
|
+
if (isPingMessage(parsed)) {
|
|
773
|
+
send({
|
|
774
|
+
type: MessageType.PONG,
|
|
775
|
+
pingId: parsed.pingId
|
|
776
|
+
});
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (isDeviceStatusMessage(parsed)) {
|
|
780
|
+
deviceStatus = parsed.payload;
|
|
781
|
+
options.onDeviceStatus?.(parsed.payload);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (isSnapshotUploadRequestedMessage(parsed)) {
|
|
785
|
+
options.onSnapshotUploadRequested?.(parsed);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (isExportSnapshotErrorMessage(parsed)) {
|
|
789
|
+
options.onExportSnapshotError?.(parsed);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (isRefreshStatusMessage(parsed)) {
|
|
793
|
+
options.onRefreshStatus?.(parsed);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (isSnapshotReadyMessage(parsed)) {
|
|
797
|
+
options.onSnapshotReady?.(parsed);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (isRefreshErrorMessage(parsed)) {
|
|
801
|
+
options.onRefreshError?.(parsed);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (isBeginTransactionMessage(parsed)) {
|
|
805
|
+
options.onBeginTransaction?.(parsed);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (isCommitTransactionMessage(parsed)) {
|
|
809
|
+
options.onCommitTransaction?.(parsed);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (isRollbackTransactionMessage(parsed)) {
|
|
813
|
+
options.onRollbackTransaction?.(parsed);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (isExecuteWriteMessage(parsed)) {
|
|
817
|
+
options.onExecuteWrite?.(parsed);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (isTransactionStatusMessage(parsed)) {
|
|
821
|
+
options.onTransactionStatus?.(parsed);
|
|
822
|
+
if (role !== DevToolsRole.BROWSER) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const pending = pendingTransaction.get(parsed.transactionId);
|
|
826
|
+
if (parsed.state === "open" && pending?.action === "begin") {
|
|
827
|
+
setTransactionState("open");
|
|
828
|
+
activeTransactionId = parsed.transactionId;
|
|
829
|
+
activeDeviceId = parsed.deviceId;
|
|
830
|
+
pending.resolve();
|
|
831
|
+
pendingTransaction.delete(parsed.transactionId);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (parsed.state === "idle" && pending && (pending.action === "commit" || pending.action === "rollback")) {
|
|
835
|
+
resetTransaction();
|
|
836
|
+
pending.resolve();
|
|
837
|
+
pendingTransaction.delete(parsed.transactionId);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (parsed.state === "failed") {
|
|
841
|
+
rejectPendingTransaction(
|
|
842
|
+
parsed.transactionId,
|
|
843
|
+
new Error(parsed.message ?? "Transaction failed")
|
|
844
|
+
);
|
|
845
|
+
resetTransaction();
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (isWriteResultMessage(parsed)) {
|
|
850
|
+
options.onWriteResult?.(parsed);
|
|
851
|
+
const pending = pendingWrites.get(parsed.writeId);
|
|
852
|
+
if (pending) {
|
|
853
|
+
pending.resolve({ rowsAffected: parsed.rowsAffected });
|
|
854
|
+
pendingWrites.delete(parsed.writeId);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (isWriteErrorMessage(parsed)) {
|
|
859
|
+
options.onWriteError?.(parsed);
|
|
860
|
+
const error = new Error(parsed.message);
|
|
861
|
+
if (parsed.writeId) {
|
|
862
|
+
const pendingWrite = pendingWrites.get(parsed.writeId);
|
|
863
|
+
if (pendingWrite) {
|
|
864
|
+
pendingWrite.reject(error);
|
|
865
|
+
pendingWrites.delete(parsed.writeId);
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
rejectPendingTransaction(parsed.transactionId, error);
|
|
869
|
+
resetTransaction();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const openSocket = () => {
|
|
874
|
+
if (socket && (socket.readyState === WebSocketImpl.OPEN || socket.readyState === WebSocketImpl.CONNECTING)) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
setConnectionState(intentionalDisconnect ? "disconnected" : connectionState === "disconnected" ? "connecting" : "reconnecting");
|
|
878
|
+
socket = new WebSocketImpl(wsUrl);
|
|
879
|
+
socket.addEventListener("open", () => {
|
|
880
|
+
reconnectScheduler.reset();
|
|
881
|
+
lastConnectionError = null;
|
|
882
|
+
setConnectionState("connected");
|
|
883
|
+
options.onConnect?.();
|
|
884
|
+
sendRegister();
|
|
885
|
+
});
|
|
886
|
+
socket.addEventListener("message", (event) => {
|
|
887
|
+
let parsed;
|
|
888
|
+
try {
|
|
889
|
+
parsed = JSON.parse(String(event.data));
|
|
890
|
+
} catch {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
handleServerMessage(parsed);
|
|
894
|
+
});
|
|
895
|
+
socket.addEventListener("close", () => {
|
|
896
|
+
socket = null;
|
|
897
|
+
options.onDisconnect?.();
|
|
898
|
+
rejectAllPending(new Error("Disconnected from DevTools hub"));
|
|
899
|
+
if (intentionalDisconnect) {
|
|
900
|
+
setConnectionState("disconnected");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
setConnectionState("reconnecting");
|
|
904
|
+
reconnectScheduler.schedule();
|
|
905
|
+
});
|
|
906
|
+
socket.addEventListener("error", () => {
|
|
907
|
+
const error = new Error(`WebSocket connection failed: ${wsUrl}`);
|
|
908
|
+
lastConnectionError = error.message;
|
|
909
|
+
options.onError?.(error);
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
const connect = () => {
|
|
913
|
+
intentionalDisconnect = false;
|
|
914
|
+
openSocket();
|
|
915
|
+
};
|
|
916
|
+
const disconnect = () => {
|
|
917
|
+
intentionalDisconnect = true;
|
|
918
|
+
reconnectScheduler.cancel();
|
|
919
|
+
socket?.close();
|
|
920
|
+
socket = null;
|
|
921
|
+
rejectAllPending(new Error("Disconnected"));
|
|
922
|
+
setConnectionState("disconnected");
|
|
923
|
+
};
|
|
924
|
+
const setServerUrl = (url) => {
|
|
925
|
+
wsUrl = url;
|
|
926
|
+
const wasConnected = connectionState === "connected" || connectionState === "connecting" || connectionState === "reconnecting";
|
|
927
|
+
disconnect();
|
|
928
|
+
intentionalDisconnect = false;
|
|
929
|
+
if (wasConnected) {
|
|
930
|
+
connect();
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const requestRefresh = (targetDeviceId) => {
|
|
934
|
+
send({
|
|
935
|
+
type: MessageType.REFRESH_REQUEST,
|
|
936
|
+
deviceId: targetDeviceId,
|
|
937
|
+
refreshType: "snapshot"
|
|
938
|
+
});
|
|
939
|
+
};
|
|
940
|
+
const requestExportSnapshot = () => {
|
|
941
|
+
send({
|
|
942
|
+
type: MessageType.EXPORT_SNAPSHOT_REQUEST,
|
|
943
|
+
refreshType: "snapshot"
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
const sendTransactionAck = (message) => {
|
|
947
|
+
send({
|
|
948
|
+
type: MessageType.TRANSACTION_ACK,
|
|
949
|
+
...message
|
|
950
|
+
});
|
|
951
|
+
};
|
|
952
|
+
const sendWriteAck = (message) => {
|
|
953
|
+
send({
|
|
954
|
+
type: MessageType.WRITE_ACK,
|
|
955
|
+
...message
|
|
956
|
+
});
|
|
957
|
+
};
|
|
958
|
+
const waitForTransaction = (transactionId, action) => {
|
|
959
|
+
return new Promise((resolve, reject) => {
|
|
960
|
+
const timer = setTimeout(() => {
|
|
961
|
+
pendingTransaction.delete(transactionId);
|
|
962
|
+
reject(new Error(`Transaction ${action} timed out`));
|
|
963
|
+
}, WRITE_TIMEOUT_MS);
|
|
964
|
+
pendingTransaction.set(transactionId, {
|
|
965
|
+
action,
|
|
966
|
+
resolve: () => {
|
|
967
|
+
clearTimeout(timer);
|
|
968
|
+
resolve();
|
|
969
|
+
},
|
|
970
|
+
reject: (error) => {
|
|
971
|
+
clearTimeout(timer);
|
|
972
|
+
reject(error);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
};
|
|
977
|
+
const beginTransaction = async (targetDeviceId) => {
|
|
978
|
+
if (role !== DevToolsRole.BROWSER) {
|
|
979
|
+
throw new Error("beginTransaction is only available for browser clients");
|
|
980
|
+
}
|
|
981
|
+
if (transactionState !== "idle") {
|
|
982
|
+
throw new Error("A transaction is already active");
|
|
983
|
+
}
|
|
984
|
+
const transactionId = generateTransactionId();
|
|
985
|
+
setTransactionState("opening");
|
|
986
|
+
activeTransactionId = transactionId;
|
|
987
|
+
activeDeviceId = targetDeviceId;
|
|
988
|
+
const promise = waitForTransaction(transactionId, "begin");
|
|
989
|
+
send({
|
|
990
|
+
type: MessageType.BEGIN_TRANSACTION_REQUEST,
|
|
991
|
+
transactionId,
|
|
992
|
+
deviceId: targetDeviceId
|
|
993
|
+
});
|
|
994
|
+
await promise;
|
|
995
|
+
};
|
|
996
|
+
const commitTransaction = async () => {
|
|
997
|
+
if (role !== DevToolsRole.BROWSER) {
|
|
998
|
+
throw new Error("commitTransaction is only available for browser clients");
|
|
999
|
+
}
|
|
1000
|
+
if (!activeTransactionId || !activeDeviceId || transactionState !== "open") {
|
|
1001
|
+
throw new Error("No open transaction");
|
|
1002
|
+
}
|
|
1003
|
+
const transactionId = activeTransactionId;
|
|
1004
|
+
const deviceIdForCommit = activeDeviceId;
|
|
1005
|
+
setTransactionState("committing");
|
|
1006
|
+
const promise = waitForTransaction(transactionId, "commit");
|
|
1007
|
+
send({
|
|
1008
|
+
type: MessageType.COMMIT_TRANSACTION_REQUEST,
|
|
1009
|
+
transactionId,
|
|
1010
|
+
deviceId: deviceIdForCommit
|
|
1011
|
+
});
|
|
1012
|
+
await promise;
|
|
1013
|
+
};
|
|
1014
|
+
const rollbackTransaction = async () => {
|
|
1015
|
+
if (role !== DevToolsRole.BROWSER) {
|
|
1016
|
+
throw new Error("rollbackTransaction is only available for browser clients");
|
|
1017
|
+
}
|
|
1018
|
+
if (!activeTransactionId || !activeDeviceId) {
|
|
1019
|
+
throw new Error("No active transaction");
|
|
1020
|
+
}
|
|
1021
|
+
const transactionId = activeTransactionId;
|
|
1022
|
+
const deviceIdForRollback = activeDeviceId;
|
|
1023
|
+
setTransactionState("rolling_back");
|
|
1024
|
+
const promise = waitForTransaction(transactionId, "rollback");
|
|
1025
|
+
send({
|
|
1026
|
+
type: MessageType.ROLLBACK_TRANSACTION_REQUEST,
|
|
1027
|
+
transactionId,
|
|
1028
|
+
deviceId: deviceIdForRollback
|
|
1029
|
+
});
|
|
1030
|
+
await promise;
|
|
1031
|
+
};
|
|
1032
|
+
const executeWrite = async (operation) => {
|
|
1033
|
+
if (role !== DevToolsRole.BROWSER) {
|
|
1034
|
+
throw new Error("executeWrite is only available for browser clients");
|
|
1035
|
+
}
|
|
1036
|
+
if (!activeTransactionId || !activeDeviceId || transactionState !== "open") {
|
|
1037
|
+
throw new Error("No open transaction");
|
|
1038
|
+
}
|
|
1039
|
+
const writeId = generateWriteId();
|
|
1040
|
+
const promise = new Promise((resolve, reject) => {
|
|
1041
|
+
const timer = setTimeout(() => {
|
|
1042
|
+
pendingWrites.delete(writeId);
|
|
1043
|
+
reject(new Error("Write operation timed out"));
|
|
1044
|
+
}, WRITE_TIMEOUT_MS);
|
|
1045
|
+
pendingWrites.set(writeId, {
|
|
1046
|
+
resolve: (result) => {
|
|
1047
|
+
clearTimeout(timer);
|
|
1048
|
+
resolve(result);
|
|
1049
|
+
},
|
|
1050
|
+
reject: (error) => {
|
|
1051
|
+
clearTimeout(timer);
|
|
1052
|
+
reject(error);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
send({
|
|
1057
|
+
type: MessageType.WRITE_REQUEST,
|
|
1058
|
+
writeId,
|
|
1059
|
+
transactionId: activeTransactionId,
|
|
1060
|
+
deviceId: activeDeviceId,
|
|
1061
|
+
operation
|
|
1062
|
+
});
|
|
1063
|
+
return promise;
|
|
1064
|
+
};
|
|
1065
|
+
return {
|
|
1066
|
+
connect,
|
|
1067
|
+
disconnect,
|
|
1068
|
+
getConnectionState: () => connectionState,
|
|
1069
|
+
getLastConnectionError: () => lastConnectionError,
|
|
1070
|
+
getDeviceStatus: () => deviceStatus,
|
|
1071
|
+
getDeviceId: () => deviceId,
|
|
1072
|
+
getServerUrl: () => wsUrl,
|
|
1073
|
+
setServerUrl,
|
|
1074
|
+
send,
|
|
1075
|
+
requestRefresh,
|
|
1076
|
+
requestExportSnapshot,
|
|
1077
|
+
sendTransactionAck,
|
|
1078
|
+
sendWriteAck,
|
|
1079
|
+
beginTransaction,
|
|
1080
|
+
commitTransaction,
|
|
1081
|
+
rollbackTransaction,
|
|
1082
|
+
executeWrite,
|
|
1083
|
+
getTransactionState: () => transactionState,
|
|
1084
|
+
getActiveTransactionId: () => activeTransactionId
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/client/fetchSnapshot.ts
|
|
1089
|
+
async function fetchSnapshot(downloadUrl) {
|
|
1090
|
+
let response;
|
|
1091
|
+
try {
|
|
1092
|
+
response = await fetch(downloadUrl);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
const networkMessage = error instanceof Error ? error.message : String(error);
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`Network error fetching snapshot from ${downloadUrl}. ${networkMessage} If the web UI runs on a different port than the hub, ensure CORS is enabled on the hub HTTP API.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
if (!response.ok) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`Failed to fetch snapshot from ${downloadUrl}: ${response.status} ${response.statusText}`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return response.arrayBuffer();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/client/snapshotUpload.ts
|
|
1108
|
+
function createSnapshotUploadBody(bytes) {
|
|
1109
|
+
return bytes;
|
|
1110
|
+
}
|
|
1111
|
+
async function uploadSnapshot(uploadUrl, bytes, options) {
|
|
1112
|
+
const headers = {
|
|
1113
|
+
"Content-Type": "application/octet-stream",
|
|
1114
|
+
[options.kindHeader]: options.kind,
|
|
1115
|
+
[options.mimeHeader]: options.mimeType
|
|
1116
|
+
};
|
|
1117
|
+
if (options.databaseName && options.nameHeader) {
|
|
1118
|
+
headers[options.nameHeader] = options.databaseName;
|
|
1119
|
+
}
|
|
1120
|
+
const response = await fetch(uploadUrl, {
|
|
1121
|
+
method: "POST",
|
|
1122
|
+
headers,
|
|
1123
|
+
body: createSnapshotUploadBody(bytes)
|
|
1124
|
+
});
|
|
1125
|
+
if (!response.ok) {
|
|
1126
|
+
throw new Error(`Upload failed with status ${response.status}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/client/handleDeviceSnapshot.ts
|
|
1131
|
+
async function handleDeviceSnapshotUpload(database, message, options = {}) {
|
|
1132
|
+
if (options.deviceId && message.deviceId !== options.deviceId) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (!database) {
|
|
1136
|
+
throw new Error("No database adapter configured");
|
|
1137
|
+
}
|
|
1138
|
+
if (!options.hubServerUrl) {
|
|
1139
|
+
throw new Error("hubServerUrl is required to upload a device snapshot");
|
|
1140
|
+
}
|
|
1141
|
+
const snapshot = await database.exportSnapshot();
|
|
1142
|
+
const hubHttp = wsUrlToHttpUrl(options.hubServerUrl);
|
|
1143
|
+
const uploadUrl = buildDeviceSnapshotUrl(hubHttp, message.deviceId);
|
|
1144
|
+
await uploadSnapshot(uploadUrl, snapshot.bytes, {
|
|
1145
|
+
kind: snapshot.kind,
|
|
1146
|
+
mimeType: snapshot.mimeType,
|
|
1147
|
+
kindHeader: SNAPSHOT_KIND_HEADER,
|
|
1148
|
+
mimeHeader: SNAPSHOT_MIME_HEADER,
|
|
1149
|
+
databaseName: database.name,
|
|
1150
|
+
nameHeader: SNAPSHOT_NAME_HEADER
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/utils/isDevToolsEnabled.ts
|
|
1155
|
+
function isDevToolsEnabled(enabled) {
|
|
1156
|
+
if (enabled === false) {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
if (enabled === true) {
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
return typeof __DEV__ !== "undefined" && __DEV__;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/client/formatSyncError.ts
|
|
1166
|
+
function formatRefreshErrorMessage(code, message) {
|
|
1167
|
+
switch (code) {
|
|
1168
|
+
case "DEVICE_OFFLINE":
|
|
1169
|
+
return "Selected device disconnected. Reconnect the mobile app and try again.";
|
|
1170
|
+
case "REFRESH_IN_PROGRESS":
|
|
1171
|
+
return "A refresh is already in progress for this device.";
|
|
1172
|
+
case "EXPORT_FAILED":
|
|
1173
|
+
return `Device failed to export or upload snapshot: ${message}`;
|
|
1174
|
+
case "UPLOAD_FAILED":
|
|
1175
|
+
return `Snapshot upload failed on the hub: ${message}`;
|
|
1176
|
+
case "TIMEOUT":
|
|
1177
|
+
return "Timed out waiting for the device to upload a snapshot.";
|
|
1178
|
+
case "SNAPSHOT_NOT_FOUND":
|
|
1179
|
+
return "Snapshot expired or was not found. Click Refresh again.";
|
|
1180
|
+
case "INVALID_REQUEST":
|
|
1181
|
+
return `Invalid refresh request: ${message}`;
|
|
1182
|
+
default:
|
|
1183
|
+
return message;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/client/handleWriteOperations.ts
|
|
1188
|
+
async function handleBeginTransaction(adapter, message, report) {
|
|
1189
|
+
if (!isWritableDatabaseAdapter(adapter)) {
|
|
1190
|
+
report({
|
|
1191
|
+
transactionId: message.transactionId,
|
|
1192
|
+
action: "begin",
|
|
1193
|
+
ok: false,
|
|
1194
|
+
message: "Database adapter does not support writes"
|
|
1195
|
+
});
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
try {
|
|
1199
|
+
await adapter.beginTransaction();
|
|
1200
|
+
report({
|
|
1201
|
+
transactionId: message.transactionId,
|
|
1202
|
+
action: "begin",
|
|
1203
|
+
ok: true
|
|
1204
|
+
});
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
report({
|
|
1207
|
+
transactionId: message.transactionId,
|
|
1208
|
+
action: "begin",
|
|
1209
|
+
ok: false,
|
|
1210
|
+
message: error instanceof Error ? error.message : "Failed to begin transaction"
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async function handleCommitTransaction(adapter, message, report) {
|
|
1215
|
+
if (!isWritableDatabaseAdapter(adapter)) {
|
|
1216
|
+
report({
|
|
1217
|
+
transactionId: message.transactionId,
|
|
1218
|
+
action: "commit",
|
|
1219
|
+
ok: false,
|
|
1220
|
+
message: "Database adapter does not support writes"
|
|
1221
|
+
});
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
await adapter.commitTransaction();
|
|
1226
|
+
report({
|
|
1227
|
+
transactionId: message.transactionId,
|
|
1228
|
+
action: "commit",
|
|
1229
|
+
ok: true
|
|
1230
|
+
});
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
report({
|
|
1233
|
+
transactionId: message.transactionId,
|
|
1234
|
+
action: "commit",
|
|
1235
|
+
ok: false,
|
|
1236
|
+
message: error instanceof Error ? error.message : "Failed to commit transaction"
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async function handleRollbackTransaction(adapter, message, report) {
|
|
1241
|
+
if (!isWritableDatabaseAdapter(adapter)) {
|
|
1242
|
+
report({
|
|
1243
|
+
transactionId: message.transactionId,
|
|
1244
|
+
action: "rollback",
|
|
1245
|
+
ok: false,
|
|
1246
|
+
message: "Database adapter does not support writes"
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
try {
|
|
1251
|
+
await adapter.rollbackTransaction();
|
|
1252
|
+
report({
|
|
1253
|
+
transactionId: message.transactionId,
|
|
1254
|
+
action: "rollback",
|
|
1255
|
+
ok: true
|
|
1256
|
+
});
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
report({
|
|
1259
|
+
transactionId: message.transactionId,
|
|
1260
|
+
action: "rollback",
|
|
1261
|
+
ok: false,
|
|
1262
|
+
message: error instanceof Error ? error.message : "Failed to roll back transaction"
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
async function handleExecuteWrite(adapter, message, report) {
|
|
1267
|
+
if (!isWritableDatabaseAdapter(adapter)) {
|
|
1268
|
+
report({
|
|
1269
|
+
writeId: message.writeId,
|
|
1270
|
+
transactionId: message.transactionId,
|
|
1271
|
+
ok: false,
|
|
1272
|
+
message: "Database adapter does not support writes"
|
|
1273
|
+
});
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const result = await adapter.executeWrite(message.operation);
|
|
1278
|
+
report({
|
|
1279
|
+
writeId: message.writeId,
|
|
1280
|
+
transactionId: message.transactionId,
|
|
1281
|
+
ok: true,
|
|
1282
|
+
rowsAffected: result.rowsAffected
|
|
1283
|
+
});
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
report({
|
|
1286
|
+
writeId: message.writeId,
|
|
1287
|
+
transactionId: message.transactionId,
|
|
1288
|
+
ok: false,
|
|
1289
|
+
message: error instanceof Error ? error.message : "Write operation failed"
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
var DevToolsContext = react.createContext(null);
|
|
1294
|
+
function useDevTools() {
|
|
1295
|
+
const context = react.useContext(DevToolsContext);
|
|
1296
|
+
if (!context) {
|
|
1297
|
+
throw new Error("useDevTools must be used within DevToolsProvider");
|
|
1298
|
+
}
|
|
1299
|
+
return context;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/utils/resolveDevToolsHost.ts
|
|
1303
|
+
var ANDROID_LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1304
|
+
var ANDROID_EMULATOR_HOST = "10.0.2.2";
|
|
1305
|
+
function isAndroidLoopbackHost(host) {
|
|
1306
|
+
return ANDROID_LOOPBACK_HOSTS.has(host.toLowerCase());
|
|
1307
|
+
}
|
|
1308
|
+
function readMetroDevHostFromConstants(constants) {
|
|
1309
|
+
const hostUri = constants?.expoConfig?.hostUri ?? constants?.expoGoConfig?.debuggerHost ?? constants?.manifest2?.extra?.expoGo?.debuggerHost ?? constants?.manifest?.debuggerHost;
|
|
1310
|
+
if (typeof hostUri !== "string" || hostUri.length === 0) {
|
|
1311
|
+
return void 0;
|
|
1312
|
+
}
|
|
1313
|
+
return hostUri.split(":")[0];
|
|
1314
|
+
}
|
|
1315
|
+
function resolveDevToolsHostFromInputs(platformOs, metroHost) {
|
|
1316
|
+
if (platformOs === "android") {
|
|
1317
|
+
if (metroHost && !isAndroidLoopbackHost(metroHost)) {
|
|
1318
|
+
return metroHost;
|
|
1319
|
+
}
|
|
1320
|
+
return ANDROID_EMULATOR_HOST;
|
|
1321
|
+
}
|
|
1322
|
+
if (metroHost) {
|
|
1323
|
+
return metroHost;
|
|
1324
|
+
}
|
|
1325
|
+
return "localhost";
|
|
1326
|
+
}
|
|
1327
|
+
function getConnectionHintForPlatform(platformOs, serverUrl) {
|
|
1328
|
+
try {
|
|
1329
|
+
const parsed = new URL(serverUrl);
|
|
1330
|
+
const host = parsed.hostname.toLowerCase();
|
|
1331
|
+
if (platformOs === "android" && isAndroidLoopbackHost(host)) {
|
|
1332
|
+
return `On Android, "${host}" refers to the device itself. Use ${ANDROID_EMULATOR_HOST} (emulator) or your PC's LAN IP (physical device).`;
|
|
1333
|
+
}
|
|
1334
|
+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
1335
|
+
return "Server URL must start with ws:// or wss://";
|
|
1336
|
+
}
|
|
1337
|
+
} catch {
|
|
1338
|
+
return "Invalid WebSocket URL.";
|
|
1339
|
+
}
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
function getPlatformOs() {
|
|
1343
|
+
try {
|
|
1344
|
+
const { Platform: Platform2 } = __require("react-native");
|
|
1345
|
+
return Platform2.OS;
|
|
1346
|
+
} catch {
|
|
1347
|
+
return "web";
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function readMetroDevHostRuntime() {
|
|
1351
|
+
try {
|
|
1352
|
+
const mod = __require("expo-constants");
|
|
1353
|
+
return readMetroDevHostFromConstants(mod.default);
|
|
1354
|
+
} catch {
|
|
1355
|
+
return void 0;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function resolveDevToolsHost() {
|
|
1359
|
+
return resolveDevToolsHostFromInputs(getPlatformOs(), readMetroDevHostRuntime());
|
|
1360
|
+
}
|
|
1361
|
+
function getConnectionHint(serverUrl) {
|
|
1362
|
+
return getConnectionHintForPlatform(getPlatformOs(), serverUrl);
|
|
1363
|
+
}
|
|
1364
|
+
function getExpoConstants() {
|
|
1365
|
+
try {
|
|
1366
|
+
const mod = __require("expo-constants");
|
|
1367
|
+
return mod.default;
|
|
1368
|
+
} catch {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function resolveDeviceMetadata() {
|
|
1373
|
+
const constants = getExpoConstants();
|
|
1374
|
+
const config = constants?.expoConfig;
|
|
1375
|
+
const metadata = {
|
|
1376
|
+
platform: reactNative.Platform.OS,
|
|
1377
|
+
appName: config?.name ?? "React Native App"
|
|
1378
|
+
};
|
|
1379
|
+
if (config?.version) {
|
|
1380
|
+
metadata.appVersion = config.version;
|
|
1381
|
+
}
|
|
1382
|
+
const bundleId = config?.ios?.bundleIdentifier ?? config?.android?.package;
|
|
1383
|
+
if (bundleId) {
|
|
1384
|
+
metadata.bundleId = bundleId;
|
|
1385
|
+
}
|
|
1386
|
+
return metadata;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/utils/resolveServerUrl.ts
|
|
1390
|
+
function normalizeServerUrl(url) {
|
|
1391
|
+
const trimmed = url.trim();
|
|
1392
|
+
if (trimmed.startsWith("ws://") || trimmed.startsWith("wss://")) {
|
|
1393
|
+
return trimmed;
|
|
1394
|
+
}
|
|
1395
|
+
return buildDevToolsWsUrl(trimmed.replace(/^https?:\/\//, ""), DEFAULT_DEVTOOLS_PORT);
|
|
1396
|
+
}
|
|
1397
|
+
function resolveServerUrl(serverUrl) {
|
|
1398
|
+
if (serverUrl) {
|
|
1399
|
+
return normalizeServerUrl(serverUrl);
|
|
1400
|
+
}
|
|
1401
|
+
const envUrl = typeof process !== "undefined" ? process.env.EXPO_PUBLIC_DATABASE_DEVTOOLS_URL : void 0;
|
|
1402
|
+
if (envUrl) {
|
|
1403
|
+
return normalizeServerUrl(envUrl);
|
|
1404
|
+
}
|
|
1405
|
+
return buildDevToolsWsUrl(resolveDevToolsHost(), DEFAULT_DEVTOOLS_PORT);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/utils/persistDeviceId.ts
|
|
1409
|
+
var PERSISTED_DEVICE_ID_STORAGE_KEY = "@database-devtools/device-id";
|
|
1410
|
+
var memoryDeviceId = null;
|
|
1411
|
+
function createAsyncStorageDeviceIdStore() {
|
|
1412
|
+
try {
|
|
1413
|
+
const AsyncStorage = __require("@react-native-async-storage/async-storage").default;
|
|
1414
|
+
return AsyncStorage;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function loadOrCreateDeviceId(storage = createAsyncStorageDeviceIdStore()) {
|
|
1420
|
+
if (storage) {
|
|
1421
|
+
const stored = await storage.getItem(PERSISTED_DEVICE_ID_STORAGE_KEY);
|
|
1422
|
+
if (stored) {
|
|
1423
|
+
return stored;
|
|
1424
|
+
}
|
|
1425
|
+
const created = generateDeviceId();
|
|
1426
|
+
await storage.setItem(PERSISTED_DEVICE_ID_STORAGE_KEY, created);
|
|
1427
|
+
return created;
|
|
1428
|
+
}
|
|
1429
|
+
if (!memoryDeviceId) {
|
|
1430
|
+
memoryDeviceId = generateDeviceId();
|
|
1431
|
+
}
|
|
1432
|
+
return memoryDeviceId;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/utils/sqlSafety.ts
|
|
1436
|
+
var BLOCKED_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE|ATTACH|DETACH|REINDEX|VACUUM|TRUNCATE)\b/i;
|
|
1437
|
+
function validateReadOnlySql(sql) {
|
|
1438
|
+
const trimmed = sql.trim();
|
|
1439
|
+
if (!trimmed) {
|
|
1440
|
+
throw new Error("SQL query is empty");
|
|
1441
|
+
}
|
|
1442
|
+
if (BLOCKED_KEYWORDS.test(trimmed)) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
"DevTools is read-only; only SELECT, PRAGMA, EXPLAIN, and WITH queries are allowed"
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const statements = trimmed.split(";").map((statement) => statement.trim()).filter(Boolean);
|
|
1448
|
+
for (const statement of statements) {
|
|
1449
|
+
const leading = statement.replace(/^\s*(--[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*/g, "").trim();
|
|
1450
|
+
const keyword = leading.split(/\s+/)[0]?.toUpperCase();
|
|
1451
|
+
if (!keyword) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if (keyword !== "SELECT" && keyword !== "PRAGMA" && keyword !== "EXPLAIN" && keyword !== "WITH") {
|
|
1455
|
+
throw new Error(`DevTools is read-only; "${keyword}" statements are not allowed`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/mobile/sqliteTableQuery.ts
|
|
1461
|
+
function quoteIdentifier2(name) {
|
|
1462
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
1463
|
+
}
|
|
1464
|
+
function buildSearchClause(columns, search) {
|
|
1465
|
+
const term = search?.trim();
|
|
1466
|
+
if (!term || columns.length === 0) {
|
|
1467
|
+
return { clause: "", params: [] };
|
|
1468
|
+
}
|
|
1469
|
+
const pattern = `%${term}%`;
|
|
1470
|
+
const conditions = columns.map(
|
|
1471
|
+
(column) => `CAST(${quoteIdentifier2(column.name)} AS TEXT) LIKE ?`
|
|
1472
|
+
);
|
|
1473
|
+
return {
|
|
1474
|
+
clause: ` WHERE (${conditions.join(" OR ")})`,
|
|
1475
|
+
params: columns.map(() => pattern)
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function resolveSortColumn(columns, sortColumn) {
|
|
1479
|
+
if (!sortColumn) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
return columns.some((column) => column.name === sortColumn) ? sortColumn : null;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/mobile/createExpoSqliteInspector.ts
|
|
1486
|
+
function normalizeCell(cell) {
|
|
1487
|
+
if (cell === null || cell === void 0) {
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof cell === "number" || typeof cell === "string") {
|
|
1491
|
+
return cell;
|
|
1492
|
+
}
|
|
1493
|
+
if (typeof cell === "boolean") {
|
|
1494
|
+
return cell ? 1 : 0;
|
|
1495
|
+
}
|
|
1496
|
+
if (cell instanceof Uint8Array) {
|
|
1497
|
+
return `[BLOB ${cell.byteLength} bytes]`;
|
|
1498
|
+
}
|
|
1499
|
+
return String(cell);
|
|
1500
|
+
}
|
|
1501
|
+
function objectRowsToMatrix(rows) {
|
|
1502
|
+
if (rows.length === 0) {
|
|
1503
|
+
return { columns: [], matrix: [] };
|
|
1504
|
+
}
|
|
1505
|
+
const columns = Object.keys(rows[0]);
|
|
1506
|
+
return {
|
|
1507
|
+
columns,
|
|
1508
|
+
matrix: rows.map((row) => columns.map((column) => normalizeCell(row[column])))
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function createExpoSqliteInspector(database) {
|
|
1512
|
+
let operationQueue = Promise.resolve();
|
|
1513
|
+
const enqueue = async (operation) => {
|
|
1514
|
+
const next = operationQueue.then(operation);
|
|
1515
|
+
operationQueue = next.then(
|
|
1516
|
+
() => void 0,
|
|
1517
|
+
() => void 0
|
|
1518
|
+
);
|
|
1519
|
+
return next;
|
|
1520
|
+
};
|
|
1521
|
+
const listTablesInternal = async () => {
|
|
1522
|
+
const masterRows = await database.getAllAsync(
|
|
1523
|
+
`SELECT name FROM sqlite_master
|
|
1524
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
1525
|
+
ORDER BY name`
|
|
1526
|
+
);
|
|
1527
|
+
const tables = [];
|
|
1528
|
+
for (const row of masterRows) {
|
|
1529
|
+
const countRow = await database.getFirstAsync(
|
|
1530
|
+
`SELECT COUNT(*) AS count FROM ${quoteIdentifier2(row.name)}`
|
|
1531
|
+
);
|
|
1532
|
+
tables.push({
|
|
1533
|
+
name: row.name,
|
|
1534
|
+
rowCount: Number(countRow?.count ?? 0)
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
return tables;
|
|
1538
|
+
};
|
|
1539
|
+
const getTableColumnsInternal = async (table) => {
|
|
1540
|
+
const rows = await database.getAllAsync(
|
|
1541
|
+
`PRAGMA table_info(${quoteIdentifier2(table)})`
|
|
1542
|
+
);
|
|
1543
|
+
return rows.map((row) => ({
|
|
1544
|
+
name: row.name,
|
|
1545
|
+
type: row.type || "ANY",
|
|
1546
|
+
notNull: row.notnull === 1,
|
|
1547
|
+
pk: row.pk === 1,
|
|
1548
|
+
defaultValue: row.dflt_value === null || row.dflt_value === void 0 ? null : String(row.dflt_value)
|
|
1549
|
+
}));
|
|
1550
|
+
};
|
|
1551
|
+
return {
|
|
1552
|
+
listTables: () => enqueue(listTablesInternal),
|
|
1553
|
+
getSchema: () => enqueue(async () => {
|
|
1554
|
+
const tables = await listTablesInternal();
|
|
1555
|
+
return Promise.all(
|
|
1556
|
+
tables.map(async (table) => ({
|
|
1557
|
+
name: table.name,
|
|
1558
|
+
columns: await getTableColumnsInternal(table.name)
|
|
1559
|
+
}))
|
|
1560
|
+
);
|
|
1561
|
+
}),
|
|
1562
|
+
getTableColumns: (table) => enqueue(() => getTableColumnsInternal(table)),
|
|
1563
|
+
getDatabaseInfo: () => enqueue(async () => {
|
|
1564
|
+
const tables = await listTablesInternal();
|
|
1565
|
+
const versionRow = await database.getFirstAsync(
|
|
1566
|
+
"SELECT sqlite_version() AS version"
|
|
1567
|
+
);
|
|
1568
|
+
const pageRow = await database.getFirstAsync(
|
|
1569
|
+
"SELECT page_size, page_count FROM pragma_page_size(), pragma_page_count()"
|
|
1570
|
+
);
|
|
1571
|
+
const pageSize = Number(pageRow?.page_size ?? 0);
|
|
1572
|
+
const pageCount = Number(pageRow?.page_count ?? 0);
|
|
1573
|
+
return {
|
|
1574
|
+
name: database.databasePath.split("/").pop() ?? database.databasePath,
|
|
1575
|
+
path: database.databasePath,
|
|
1576
|
+
tableCount: tables.length,
|
|
1577
|
+
sqliteVersion: versionRow?.version ?? "unknown",
|
|
1578
|
+
pageSize,
|
|
1579
|
+
pageCount,
|
|
1580
|
+
estimatedSizeBytes: pageSize * pageCount
|
|
1581
|
+
};
|
|
1582
|
+
}),
|
|
1583
|
+
fetchTablePage: (request) => enqueue(async () => {
|
|
1584
|
+
const columns = await getTableColumnsInternal(request.table);
|
|
1585
|
+
const { clause, params: searchParams } = buildSearchClause(columns, request.search);
|
|
1586
|
+
const quotedTable = quoteIdentifier2(request.table);
|
|
1587
|
+
const countRow = await database.getFirstAsync(
|
|
1588
|
+
`SELECT COUNT(*) AS count FROM ${quotedTable}${clause}`,
|
|
1589
|
+
searchParams
|
|
1590
|
+
);
|
|
1591
|
+
const totalCount = Number(countRow?.count ?? 0);
|
|
1592
|
+
const pageSize = Math.max(1, request.pageSize);
|
|
1593
|
+
const page = Math.max(1, request.page);
|
|
1594
|
+
const offset = (page - 1) * pageSize;
|
|
1595
|
+
const sortColumn = resolveSortColumn(columns, request.sortColumn);
|
|
1596
|
+
const orderClause = sortColumn ? ` ORDER BY ${quoteIdentifier2(sortColumn)} ${request.sortDir === "desc" ? "DESC" : "ASC"}` : "";
|
|
1597
|
+
const rows = await database.getAllAsync(
|
|
1598
|
+
`SELECT * FROM ${quotedTable}${clause}${orderClause} LIMIT ? OFFSET ?`,
|
|
1599
|
+
[...searchParams, pageSize, offset]
|
|
1600
|
+
);
|
|
1601
|
+
const { columns: resultColumns, matrix } = objectRowsToMatrix(rows);
|
|
1602
|
+
return {
|
|
1603
|
+
columns: resultColumns,
|
|
1604
|
+
rows: matrix,
|
|
1605
|
+
totalCount,
|
|
1606
|
+
page,
|
|
1607
|
+
pageSize
|
|
1608
|
+
};
|
|
1609
|
+
}),
|
|
1610
|
+
executeQuery: (sql) => enqueue(async () => {
|
|
1611
|
+
validateReadOnlySql(sql);
|
|
1612
|
+
const started = Date.now();
|
|
1613
|
+
const rows = await database.getAllAsync(sql);
|
|
1614
|
+
const { columns, matrix } = objectRowsToMatrix(rows);
|
|
1615
|
+
return {
|
|
1616
|
+
columns,
|
|
1617
|
+
rows: matrix,
|
|
1618
|
+
rowCount: matrix.length,
|
|
1619
|
+
durationMs: Date.now() - started
|
|
1620
|
+
};
|
|
1621
|
+
})
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function DevToolsProvider({
|
|
1625
|
+
children,
|
|
1626
|
+
database,
|
|
1627
|
+
type,
|
|
1628
|
+
adapter: explicitAdapter,
|
|
1629
|
+
serverUrl: initialServerUrl,
|
|
1630
|
+
onConnectionStateChange
|
|
1631
|
+
}) {
|
|
1632
|
+
const [connectionState, setConnectionState] = react.useState("connecting");
|
|
1633
|
+
const [connectionError, setConnectionError] = react.useState(null);
|
|
1634
|
+
const [serverUrl, setServerUrl] = react.useState(() => resolveServerUrl(initialServerUrl));
|
|
1635
|
+
const [deviceId, setDeviceId] = react.useState();
|
|
1636
|
+
const [deviceIdReady, setDeviceIdReady] = react.useState(false);
|
|
1637
|
+
const [settingsVisible, setSettingsVisible] = react.useState(false);
|
|
1638
|
+
const [launcherVisible, setLauncherVisible] = react.useState(false);
|
|
1639
|
+
const [explorerVisible, setExplorerVisible] = react.useState(false);
|
|
1640
|
+
const [mobileInspector, setMobileInspector] = react.useState(null);
|
|
1641
|
+
const [resolvedAdapter, setResolvedAdapter] = react.useState(explicitAdapter);
|
|
1642
|
+
const [adapterError, setAdapterError] = react.useState(null);
|
|
1643
|
+
const [exportState, setExportState] = react.useState("idle");
|
|
1644
|
+
const [exportError, setExportError] = react.useState(null);
|
|
1645
|
+
const metadata = react.useMemo(() => resolveDeviceMetadata(), []);
|
|
1646
|
+
const clientRef = react.useRef(null);
|
|
1647
|
+
const onConnectionStateChangeRef = react.useRef(onConnectionStateChange);
|
|
1648
|
+
const databaseRef = react.useRef(resolvedAdapter);
|
|
1649
|
+
const rollbackOpenTransactionRef = react.useRef(null);
|
|
1650
|
+
const exportWaitersRef = react.useRef(null);
|
|
1651
|
+
const exportTimeoutRef = react.useRef(null);
|
|
1652
|
+
const exportSuccessTimeoutRef = react.useRef(null);
|
|
1653
|
+
const clearExportWaiters = react.useCallback(() => {
|
|
1654
|
+
if (exportTimeoutRef.current) {
|
|
1655
|
+
clearTimeout(exportTimeoutRef.current);
|
|
1656
|
+
exportTimeoutRef.current = null;
|
|
1657
|
+
}
|
|
1658
|
+
exportWaitersRef.current = null;
|
|
1659
|
+
}, []);
|
|
1660
|
+
const resetExportSuccessLater = react.useCallback(() => {
|
|
1661
|
+
if (exportSuccessTimeoutRef.current) {
|
|
1662
|
+
clearTimeout(exportSuccessTimeoutRef.current);
|
|
1663
|
+
}
|
|
1664
|
+
exportSuccessTimeoutRef.current = setTimeout(() => {
|
|
1665
|
+
setExportState((current) => current === "success" ? "idle" : current);
|
|
1666
|
+
exportSuccessTimeoutRef.current = null;
|
|
1667
|
+
}, 3e3);
|
|
1668
|
+
}, []);
|
|
1669
|
+
const finishExportSuccess = react.useCallback(() => {
|
|
1670
|
+
clearExportWaiters();
|
|
1671
|
+
setExportState("success");
|
|
1672
|
+
setExportError(null);
|
|
1673
|
+
resetExportSuccessLater();
|
|
1674
|
+
}, [clearExportWaiters, resetExportSuccessLater]);
|
|
1675
|
+
const finishExportFailure = react.useCallback(
|
|
1676
|
+
(message) => {
|
|
1677
|
+
clearExportWaiters();
|
|
1678
|
+
setExportState("error");
|
|
1679
|
+
setExportError(message);
|
|
1680
|
+
},
|
|
1681
|
+
[clearExportWaiters]
|
|
1682
|
+
);
|
|
1683
|
+
react.useEffect(() => {
|
|
1684
|
+
onConnectionStateChangeRef.current = onConnectionStateChange;
|
|
1685
|
+
}, [onConnectionStateChange]);
|
|
1686
|
+
react.useEffect(() => {
|
|
1687
|
+
let cancelled = false;
|
|
1688
|
+
async function resolveDatabaseAdapter() {
|
|
1689
|
+
if (explicitAdapter) {
|
|
1690
|
+
setResolvedAdapter(explicitAdapter);
|
|
1691
|
+
setAdapterError(null);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (!database) {
|
|
1695
|
+
setResolvedAdapter(void 0);
|
|
1696
|
+
setAdapterError(null);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
try {
|
|
1700
|
+
const nextAdapter = await resolveAdapter(database, { type });
|
|
1701
|
+
if (!cancelled) {
|
|
1702
|
+
setResolvedAdapter(nextAdapter);
|
|
1703
|
+
setAdapterError(null);
|
|
1704
|
+
}
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
if (!cancelled) {
|
|
1707
|
+
setResolvedAdapter(void 0);
|
|
1708
|
+
setAdapterError(error instanceof Error ? error.message : "Failed to resolve database adapter");
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
void resolveDatabaseAdapter();
|
|
1713
|
+
return () => {
|
|
1714
|
+
cancelled = true;
|
|
1715
|
+
};
|
|
1716
|
+
}, [database, type, explicitAdapter]);
|
|
1717
|
+
react.useEffect(() => {
|
|
1718
|
+
if (detectExpoSqliteInspectable(database)) {
|
|
1719
|
+
setMobileInspector(createExpoSqliteInspector(database));
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
setMobileInspector(null);
|
|
1723
|
+
}, [database]);
|
|
1724
|
+
react.useEffect(() => {
|
|
1725
|
+
databaseRef.current = resolvedAdapter;
|
|
1726
|
+
}, [resolvedAdapter]);
|
|
1727
|
+
react.useEffect(() => {
|
|
1728
|
+
rollbackOpenTransactionRef.current = async () => {
|
|
1729
|
+
const adapter = databaseRef.current;
|
|
1730
|
+
if (!isWritableDatabaseAdapter(adapter)) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
try {
|
|
1734
|
+
await adapter.rollbackTransaction();
|
|
1735
|
+
} catch {
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
}, [resolvedAdapter]);
|
|
1739
|
+
react.useEffect(() => {
|
|
1740
|
+
let cancelled = false;
|
|
1741
|
+
void loadOrCreateDeviceId().then((persistedId) => {
|
|
1742
|
+
if (!cancelled) {
|
|
1743
|
+
setDeviceId(persistedId);
|
|
1744
|
+
setDeviceIdReady(true);
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
return () => {
|
|
1748
|
+
cancelled = true;
|
|
1749
|
+
};
|
|
1750
|
+
}, []);
|
|
1751
|
+
react.useEffect(() => {
|
|
1752
|
+
if (!deviceIdReady || !deviceId) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
const client = createDevToolsClient({
|
|
1756
|
+
serverUrl,
|
|
1757
|
+
role: DevToolsRole.MOBILE,
|
|
1758
|
+
deviceId,
|
|
1759
|
+
metadata,
|
|
1760
|
+
onConnect: () => {
|
|
1761
|
+
setConnectionError(null);
|
|
1762
|
+
},
|
|
1763
|
+
onConnectionStateChange: (state) => {
|
|
1764
|
+
setConnectionState(state);
|
|
1765
|
+
onConnectionStateChangeRef.current?.(state);
|
|
1766
|
+
},
|
|
1767
|
+
onError: (error) => {
|
|
1768
|
+
setConnectionError(error.message);
|
|
1769
|
+
},
|
|
1770
|
+
onSnapshotUploadRequested: async (message) => {
|
|
1771
|
+
try {
|
|
1772
|
+
await handleDeviceSnapshotUpload(databaseRef.current, message, {
|
|
1773
|
+
hubServerUrl: serverUrl,
|
|
1774
|
+
deviceId: clientRef.current?.getDeviceId()
|
|
1775
|
+
});
|
|
1776
|
+
if (exportWaitersRef.current) {
|
|
1777
|
+
exportWaitersRef.current.resolve();
|
|
1778
|
+
clearExportWaiters();
|
|
1779
|
+
}
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
const errorMessage = error instanceof Error ? error.message : "Database export or upload failed";
|
|
1782
|
+
if (exportWaitersRef.current) {
|
|
1783
|
+
exportWaitersRef.current.reject(new Error(errorMessage));
|
|
1784
|
+
clearExportWaiters();
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
console.error("[database-devtools] Snapshot export or upload failed:", errorMessage);
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
onExportSnapshotError: (message) => {
|
|
1791
|
+
const errorMessage = formatRefreshErrorMessage(message.code, message.message);
|
|
1792
|
+
if (exportWaitersRef.current) {
|
|
1793
|
+
exportWaitersRef.current.reject(new Error(errorMessage));
|
|
1794
|
+
clearExportWaiters();
|
|
1795
|
+
}
|
|
1796
|
+
},
|
|
1797
|
+
onBeginTransaction: async (message) => {
|
|
1798
|
+
const client2 = clientRef.current;
|
|
1799
|
+
if (!client2) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
await handleBeginTransaction(databaseRef.current, message, (ack) => {
|
|
1803
|
+
client2.sendTransactionAck(ack);
|
|
1804
|
+
});
|
|
1805
|
+
},
|
|
1806
|
+
onCommitTransaction: async (message) => {
|
|
1807
|
+
const client2 = clientRef.current;
|
|
1808
|
+
if (!client2) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
await handleCommitTransaction(databaseRef.current, message, (ack) => {
|
|
1812
|
+
client2.sendTransactionAck(ack);
|
|
1813
|
+
});
|
|
1814
|
+
},
|
|
1815
|
+
onRollbackTransaction: async (message) => {
|
|
1816
|
+
const client2 = clientRef.current;
|
|
1817
|
+
if (!client2) {
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
await handleRollbackTransaction(databaseRef.current, message, (ack) => {
|
|
1821
|
+
client2.sendTransactionAck(ack);
|
|
1822
|
+
});
|
|
1823
|
+
},
|
|
1824
|
+
onExecuteWrite: async (message) => {
|
|
1825
|
+
const client2 = clientRef.current;
|
|
1826
|
+
if (!client2) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
await handleExecuteWrite(databaseRef.current, message, (ack) => {
|
|
1830
|
+
client2.sendWriteAck(ack);
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
clientRef.current = client;
|
|
1835
|
+
setDeviceId(client.getDeviceId());
|
|
1836
|
+
client.connect();
|
|
1837
|
+
return () => {
|
|
1838
|
+
void rollbackOpenTransactionRef.current?.();
|
|
1839
|
+
client.disconnect();
|
|
1840
|
+
clientRef.current = null;
|
|
1841
|
+
clearExportWaiters();
|
|
1842
|
+
if (exportSuccessTimeoutRef.current) {
|
|
1843
|
+
clearTimeout(exportSuccessTimeoutRef.current);
|
|
1844
|
+
exportSuccessTimeoutRef.current = null;
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
}, [serverUrl, metadata, clearExportWaiters, deviceIdReady, deviceId]);
|
|
1848
|
+
const openLauncher = react.useCallback(() => {
|
|
1849
|
+
setLauncherVisible(true);
|
|
1850
|
+
}, []);
|
|
1851
|
+
const closeLauncher = react.useCallback(() => {
|
|
1852
|
+
setLauncherVisible(false);
|
|
1853
|
+
}, []);
|
|
1854
|
+
const openExplorer = react.useCallback(() => {
|
|
1855
|
+
setLauncherVisible(false);
|
|
1856
|
+
setExplorerVisible(true);
|
|
1857
|
+
}, []);
|
|
1858
|
+
const closeExplorer = react.useCallback(() => {
|
|
1859
|
+
setExplorerVisible(false);
|
|
1860
|
+
}, []);
|
|
1861
|
+
const openSettings = react.useCallback(() => {
|
|
1862
|
+
setLauncherVisible(false);
|
|
1863
|
+
setSettingsVisible(true);
|
|
1864
|
+
}, []);
|
|
1865
|
+
const closeSettings = react.useCallback(() => {
|
|
1866
|
+
setSettingsVisible(false);
|
|
1867
|
+
}, []);
|
|
1868
|
+
const reconnect = react.useCallback((url) => {
|
|
1869
|
+
setConnectionError(null);
|
|
1870
|
+
setServerUrl(normalizeServerUrl(url));
|
|
1871
|
+
}, []);
|
|
1872
|
+
const exportDatabase = react.useCallback(async () => {
|
|
1873
|
+
if (exportState === "exporting") {
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (connectionState !== "connected") {
|
|
1877
|
+
finishExportFailure("Connect to the DevTools hub first");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (!resolvedAdapter) {
|
|
1881
|
+
finishExportFailure(adapterError ?? "No database adapter connected");
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const client = clientRef.current;
|
|
1885
|
+
if (!client) {
|
|
1886
|
+
finishExportFailure("DevTools client is not ready");
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
setExportState("exporting");
|
|
1890
|
+
setExportError(null);
|
|
1891
|
+
try {
|
|
1892
|
+
await new Promise((resolve, reject) => {
|
|
1893
|
+
exportWaitersRef.current = { resolve, reject };
|
|
1894
|
+
exportTimeoutRef.current = setTimeout(() => {
|
|
1895
|
+
exportWaitersRef.current?.reject(new Error("Export timed out"));
|
|
1896
|
+
clearExportWaiters();
|
|
1897
|
+
}, REFRESH_TIMEOUT_MS);
|
|
1898
|
+
client.requestExportSnapshot();
|
|
1899
|
+
});
|
|
1900
|
+
finishExportSuccess();
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
const message = error instanceof Error ? error.message : "Export failed";
|
|
1903
|
+
finishExportFailure(message);
|
|
1904
|
+
}
|
|
1905
|
+
}, [
|
|
1906
|
+
adapterError,
|
|
1907
|
+
connectionState,
|
|
1908
|
+
exportState,
|
|
1909
|
+
finishExportFailure,
|
|
1910
|
+
resolvedAdapter
|
|
1911
|
+
]);
|
|
1912
|
+
const connectionHint = react.useMemo(() => getConnectionHint(serverUrl), [serverUrl]);
|
|
1913
|
+
const value = react.useMemo(
|
|
1914
|
+
() => ({
|
|
1915
|
+
connectionState,
|
|
1916
|
+
connectionError,
|
|
1917
|
+
connectionHint,
|
|
1918
|
+
deviceId,
|
|
1919
|
+
serverUrl,
|
|
1920
|
+
metadata,
|
|
1921
|
+
database: resolvedAdapter,
|
|
1922
|
+
adapterError,
|
|
1923
|
+
mobileInspector,
|
|
1924
|
+
launcherVisible,
|
|
1925
|
+
openLauncher,
|
|
1926
|
+
closeLauncher,
|
|
1927
|
+
explorerVisible,
|
|
1928
|
+
openExplorer,
|
|
1929
|
+
closeExplorer,
|
|
1930
|
+
settingsVisible,
|
|
1931
|
+
openSettings,
|
|
1932
|
+
closeSettings,
|
|
1933
|
+
reconnect,
|
|
1934
|
+
exportState,
|
|
1935
|
+
exportError,
|
|
1936
|
+
exportDatabase
|
|
1937
|
+
}),
|
|
1938
|
+
[
|
|
1939
|
+
connectionState,
|
|
1940
|
+
connectionError,
|
|
1941
|
+
connectionHint,
|
|
1942
|
+
deviceId,
|
|
1943
|
+
serverUrl,
|
|
1944
|
+
metadata,
|
|
1945
|
+
resolvedAdapter,
|
|
1946
|
+
adapterError,
|
|
1947
|
+
mobileInspector,
|
|
1948
|
+
launcherVisible,
|
|
1949
|
+
openLauncher,
|
|
1950
|
+
closeLauncher,
|
|
1951
|
+
explorerVisible,
|
|
1952
|
+
openExplorer,
|
|
1953
|
+
closeExplorer,
|
|
1954
|
+
settingsVisible,
|
|
1955
|
+
openSettings,
|
|
1956
|
+
closeSettings,
|
|
1957
|
+
reconnect,
|
|
1958
|
+
exportState,
|
|
1959
|
+
exportError,
|
|
1960
|
+
exportDatabase
|
|
1961
|
+
]
|
|
1962
|
+
);
|
|
1963
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DevToolsContext.Provider, { value, children });
|
|
1964
|
+
}
|
|
1965
|
+
function getStatusConfig(state) {
|
|
1966
|
+
switch (state) {
|
|
1967
|
+
case "connected":
|
|
1968
|
+
return { label: "Connected", color: "#166534", backgroundColor: "#dcfce7" };
|
|
1969
|
+
case "reconnecting":
|
|
1970
|
+
return { label: "Reconnecting", color: "#92400e", backgroundColor: "#fef3c7" };
|
|
1971
|
+
case "connecting":
|
|
1972
|
+
return { label: "Connecting", color: "#92400e", backgroundColor: "#fef3c7" };
|
|
1973
|
+
case "disconnected":
|
|
1974
|
+
return { label: "Disconnected", color: "#991b1b", backgroundColor: "#fee2e2" };
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
function getConnectionDotColor(state) {
|
|
1978
|
+
switch (state) {
|
|
1979
|
+
case "connected":
|
|
1980
|
+
return "#22c55e";
|
|
1981
|
+
case "reconnecting":
|
|
1982
|
+
case "connecting":
|
|
1983
|
+
return "#eab308";
|
|
1984
|
+
case "disconnected":
|
|
1985
|
+
return "#ef4444";
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function ConnectionStatusBadge({ state }) {
|
|
1989
|
+
const config = getStatusConfig(state);
|
|
1990
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.badge, { backgroundColor: config.backgroundColor }], children: [
|
|
1991
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.dot, { backgroundColor: getConnectionDotColor(state) }] }),
|
|
1992
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.label, { color: config.color }], children: config.label })
|
|
1993
|
+
] });
|
|
1994
|
+
}
|
|
1995
|
+
var styles = reactNative.StyleSheet.create({
|
|
1996
|
+
badge: {
|
|
1997
|
+
flexDirection: "row",
|
|
1998
|
+
alignItems: "center",
|
|
1999
|
+
alignSelf: "flex-start",
|
|
2000
|
+
paddingHorizontal: 10,
|
|
2001
|
+
paddingVertical: 6,
|
|
2002
|
+
borderRadius: 999,
|
|
2003
|
+
gap: 6
|
|
2004
|
+
},
|
|
2005
|
+
dot: {
|
|
2006
|
+
width: 8,
|
|
2007
|
+
height: 8,
|
|
2008
|
+
borderRadius: 4
|
|
2009
|
+
},
|
|
2010
|
+
label: {
|
|
2011
|
+
fontSize: 13,
|
|
2012
|
+
fontWeight: "600"
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
function DevToolsLauncherModal() {
|
|
2016
|
+
const {
|
|
2017
|
+
launcherVisible,
|
|
2018
|
+
closeLauncher,
|
|
2019
|
+
openExplorer,
|
|
2020
|
+
openSettings,
|
|
2021
|
+
connectionState,
|
|
2022
|
+
mobileInspector,
|
|
2023
|
+
exportState,
|
|
2024
|
+
exportError,
|
|
2025
|
+
exportDatabase,
|
|
2026
|
+
database,
|
|
2027
|
+
adapterError
|
|
2028
|
+
} = useDevTools();
|
|
2029
|
+
const isExporting = exportState === "exporting";
|
|
2030
|
+
const exportDisabled = connectionState !== "connected" || !database || isExporting || Boolean(adapterError);
|
|
2031
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2032
|
+
reactNative.Modal,
|
|
2033
|
+
{
|
|
2034
|
+
animationType: "slide",
|
|
2035
|
+
onRequestClose: closeLauncher,
|
|
2036
|
+
transparent: true,
|
|
2037
|
+
visible: launcherVisible,
|
|
2038
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.overlay, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.sheet, children: [
|
|
2039
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.header, children: [
|
|
2040
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.title, children: "Database DevTools" }),
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { accessibilityLabel: "Close launcher", onPress: closeLauncher, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.closeButton, children: "Close" }) })
|
|
2042
|
+
] }),
|
|
2043
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.body, children: [
|
|
2044
|
+
/* @__PURE__ */ jsxRuntime.jsx(ConnectionStatusBadge, { state: connectionState }),
|
|
2045
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.hint, children: "Browse and query the on-device database, or connect to the web inspector." }),
|
|
2046
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2047
|
+
reactNative.Pressable,
|
|
2048
|
+
{
|
|
2049
|
+
accessibilityLabel: "View database on device",
|
|
2050
|
+
disabled: !mobileInspector,
|
|
2051
|
+
onPress: openExplorer,
|
|
2052
|
+
style: [styles2.primaryButton, !mobileInspector && styles2.primaryButtonDisabled],
|
|
2053
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.primaryLabel, children: "View Database" })
|
|
2054
|
+
}
|
|
2055
|
+
),
|
|
2056
|
+
!mobileInspector ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.warning, children: "On-device explorer requires an Expo SQLite database instance." }) : null,
|
|
2057
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2058
|
+
reactNative.Pressable,
|
|
2059
|
+
{
|
|
2060
|
+
accessibilityLabel: "Open DevTools settings",
|
|
2061
|
+
onPress: openSettings,
|
|
2062
|
+
style: styles2.secondaryButton,
|
|
2063
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.secondaryLabel, children: "DevTools Settings" })
|
|
2064
|
+
}
|
|
2065
|
+
),
|
|
2066
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2067
|
+
reactNative.Pressable,
|
|
2068
|
+
{
|
|
2069
|
+
accessibilityLabel: "Export database to web inspector",
|
|
2070
|
+
accessibilityState: { busy: isExporting, disabled: exportDisabled },
|
|
2071
|
+
disabled: exportDisabled,
|
|
2072
|
+
onPress: () => void exportDatabase(),
|
|
2073
|
+
style: [styles2.ghostButton, exportDisabled && !isExporting && styles2.buttonDisabled],
|
|
2074
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.exportContent, children: [
|
|
2075
|
+
isExporting ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: "#0f172a", size: "small" }) : null,
|
|
2076
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.ghostLabel, children: isExporting ? "Exporting\u2026" : "Export to Web Inspector" })
|
|
2077
|
+
] })
|
|
2078
|
+
}
|
|
2079
|
+
),
|
|
2080
|
+
connectionState !== "connected" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.hint, children: "Connect to the hub in settings before exporting." }) : null,
|
|
2081
|
+
exportState === "success" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.success, children: "Sent to web inspector" }) : null,
|
|
2082
|
+
exportError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.error, children: exportError }) : null,
|
|
2083
|
+
adapterError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.error, children: adapterError }) : null
|
|
2084
|
+
] })
|
|
2085
|
+
] }) })
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
var styles2 = reactNative.StyleSheet.create({
|
|
2090
|
+
overlay: {
|
|
2091
|
+
flex: 1,
|
|
2092
|
+
backgroundColor: "rgba(0, 0, 0, 0.45)",
|
|
2093
|
+
justifyContent: "flex-end"
|
|
2094
|
+
},
|
|
2095
|
+
sheet: {
|
|
2096
|
+
backgroundColor: "#ffffff",
|
|
2097
|
+
borderTopLeftRadius: 16,
|
|
2098
|
+
borderTopRightRadius: 16,
|
|
2099
|
+
overflow: "hidden"
|
|
2100
|
+
},
|
|
2101
|
+
header: {
|
|
2102
|
+
flexDirection: "row",
|
|
2103
|
+
alignItems: "center",
|
|
2104
|
+
justifyContent: "space-between",
|
|
2105
|
+
paddingHorizontal: 20,
|
|
2106
|
+
paddingVertical: 16,
|
|
2107
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2108
|
+
borderBottomColor: "#e2e8f0"
|
|
2109
|
+
},
|
|
2110
|
+
title: {
|
|
2111
|
+
fontSize: 18,
|
|
2112
|
+
fontWeight: "700",
|
|
2113
|
+
color: "#0f172a"
|
|
2114
|
+
},
|
|
2115
|
+
closeButton: {
|
|
2116
|
+
fontSize: 16,
|
|
2117
|
+
color: "#2563eb",
|
|
2118
|
+
fontWeight: "600"
|
|
2119
|
+
},
|
|
2120
|
+
body: {
|
|
2121
|
+
padding: 20,
|
|
2122
|
+
gap: 12
|
|
2123
|
+
},
|
|
2124
|
+
hint: {
|
|
2125
|
+
fontSize: 13,
|
|
2126
|
+
color: "#64748b",
|
|
2127
|
+
lineHeight: 20
|
|
2128
|
+
},
|
|
2129
|
+
warning: {
|
|
2130
|
+
fontSize: 12,
|
|
2131
|
+
color: "#b45309",
|
|
2132
|
+
marginTop: -4
|
|
2133
|
+
},
|
|
2134
|
+
primaryButton: {
|
|
2135
|
+
backgroundColor: "#2563eb",
|
|
2136
|
+
borderRadius: 8,
|
|
2137
|
+
paddingVertical: 14,
|
|
2138
|
+
alignItems: "center"
|
|
2139
|
+
},
|
|
2140
|
+
primaryButtonDisabled: {
|
|
2141
|
+
opacity: 0.5
|
|
2142
|
+
},
|
|
2143
|
+
primaryLabel: {
|
|
2144
|
+
color: "#f8fafc",
|
|
2145
|
+
fontSize: 16,
|
|
2146
|
+
fontWeight: "700"
|
|
2147
|
+
},
|
|
2148
|
+
secondaryButton: {
|
|
2149
|
+
backgroundColor: "#1e293b",
|
|
2150
|
+
borderRadius: 8,
|
|
2151
|
+
paddingVertical: 12,
|
|
2152
|
+
alignItems: "center"
|
|
2153
|
+
},
|
|
2154
|
+
secondaryLabel: {
|
|
2155
|
+
color: "#f8fafc",
|
|
2156
|
+
fontSize: 15,
|
|
2157
|
+
fontWeight: "600"
|
|
2158
|
+
},
|
|
2159
|
+
ghostButton: {
|
|
2160
|
+
borderWidth: 1,
|
|
2161
|
+
borderColor: "#cbd5e1",
|
|
2162
|
+
borderRadius: 8,
|
|
2163
|
+
paddingVertical: 12,
|
|
2164
|
+
alignItems: "center"
|
|
2165
|
+
},
|
|
2166
|
+
buttonDisabled: {
|
|
2167
|
+
opacity: 0.5
|
|
2168
|
+
},
|
|
2169
|
+
ghostLabel: {
|
|
2170
|
+
color: "#0f172a",
|
|
2171
|
+
fontSize: 15,
|
|
2172
|
+
fontWeight: "600"
|
|
2173
|
+
},
|
|
2174
|
+
exportContent: {
|
|
2175
|
+
flexDirection: "row",
|
|
2176
|
+
alignItems: "center",
|
|
2177
|
+
gap: 8
|
|
2178
|
+
},
|
|
2179
|
+
success: {
|
|
2180
|
+
fontSize: 13,
|
|
2181
|
+
color: "#15803d"
|
|
2182
|
+
},
|
|
2183
|
+
error: {
|
|
2184
|
+
fontSize: 13,
|
|
2185
|
+
color: "#dc2626"
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
function DevToolsSettingsModal() {
|
|
2189
|
+
const {
|
|
2190
|
+
settingsVisible,
|
|
2191
|
+
closeSettings,
|
|
2192
|
+
connectionState,
|
|
2193
|
+
connectionError,
|
|
2194
|
+
connectionHint,
|
|
2195
|
+
deviceId,
|
|
2196
|
+
serverUrl,
|
|
2197
|
+
metadata,
|
|
2198
|
+
database,
|
|
2199
|
+
adapterError,
|
|
2200
|
+
reconnect,
|
|
2201
|
+
exportState,
|
|
2202
|
+
exportError,
|
|
2203
|
+
exportDatabase
|
|
2204
|
+
} = useDevTools();
|
|
2205
|
+
const [draftUrl, setDraftUrl] = react.useState(serverUrl);
|
|
2206
|
+
react.useEffect(() => {
|
|
2207
|
+
if (settingsVisible) {
|
|
2208
|
+
setDraftUrl(serverUrl);
|
|
2209
|
+
}
|
|
2210
|
+
}, [settingsVisible, serverUrl]);
|
|
2211
|
+
const handleReconnect = () => {
|
|
2212
|
+
const trimmed = draftUrl.trim();
|
|
2213
|
+
if (trimmed) {
|
|
2214
|
+
reconnect(trimmed);
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
const handleExportDatabase = () => {
|
|
2218
|
+
void exportDatabase();
|
|
2219
|
+
};
|
|
2220
|
+
const isExporting = exportState === "exporting";
|
|
2221
|
+
const exportDisabled = connectionState !== "connected" || !database || isExporting;
|
|
2222
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2223
|
+
reactNative.Modal,
|
|
2224
|
+
{
|
|
2225
|
+
animationType: "slide",
|
|
2226
|
+
onRequestClose: closeSettings,
|
|
2227
|
+
transparent: true,
|
|
2228
|
+
visible: settingsVisible,
|
|
2229
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.overlay, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.sheet, children: [
|
|
2230
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.header, children: [
|
|
2231
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.title, children: "Database DevTools" }),
|
|
2232
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { accessibilityLabel: "Close settings", onPress: closeSettings, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.closeButton, children: "Close" }) })
|
|
2233
|
+
] }),
|
|
2234
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2235
|
+
reactNative.ScrollView,
|
|
2236
|
+
{
|
|
2237
|
+
contentContainerStyle: styles3.content,
|
|
2238
|
+
style: styles3.scrollBody,
|
|
2239
|
+
children: [
|
|
2240
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Connection", children: [
|
|
2241
|
+
/* @__PURE__ */ jsxRuntime.jsx(ConnectionStatusBadge, { state: connectionState }),
|
|
2242
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Server URL", value: serverUrl, mono: true }),
|
|
2243
|
+
connectionError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.errorText, children: connectionError }) : null,
|
|
2244
|
+
connectionHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.hintText, children: connectionHint }) : null,
|
|
2245
|
+
connectionState === "reconnecting" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.hintText, children: "Ensure the inspector hub is running (npx database-devtools) and the URL reaches your development machine \u2014 not the phone itself." }) : null
|
|
2246
|
+
] }),
|
|
2247
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Device", children: [
|
|
2248
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Device ID", value: deviceId ?? "\u2014", mono: true }),
|
|
2249
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Platform", value: metadata.platform ?? "\u2014" }),
|
|
2250
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "App", value: metadata.appName ?? "\u2014" }),
|
|
2251
|
+
metadata.appVersion ? /* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Version", value: metadata.appVersion }) : null,
|
|
2252
|
+
metadata.bundleId ? /* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Bundle ID", value: metadata.bundleId, mono: true }) : null
|
|
2253
|
+
] }),
|
|
2254
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Server", children: [
|
|
2255
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.fieldLabel, children: "WebSocket URL" }),
|
|
2256
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2257
|
+
reactNative.TextInput,
|
|
2258
|
+
{
|
|
2259
|
+
autoCapitalize: "none",
|
|
2260
|
+
autoCorrect: false,
|
|
2261
|
+
onChangeText: setDraftUrl,
|
|
2262
|
+
placeholder: "ws://10.0.2.2:3847/ws",
|
|
2263
|
+
style: styles3.input,
|
|
2264
|
+
value: draftUrl
|
|
2265
|
+
}
|
|
2266
|
+
),
|
|
2267
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: handleReconnect, style: styles3.reconnectButton, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.reconnectLabel, children: "Reconnect" }) })
|
|
2268
|
+
] }),
|
|
2269
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Database", children: database ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2270
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Kind", value: database.kind }),
|
|
2271
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "Name", value: database.name }),
|
|
2272
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoRow, { label: "ID", value: database.id, mono: true })
|
|
2273
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.placeholder, children: adapterError ? "Adapter not connected" : "No adapter connected" }) })
|
|
2274
|
+
]
|
|
2275
|
+
}
|
|
2276
|
+
),
|
|
2277
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.exportFooter, children: [
|
|
2278
|
+
adapterError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.errorText, children: adapterError }) : null,
|
|
2279
|
+
database ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2280
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2281
|
+
reactNative.Pressable,
|
|
2282
|
+
{
|
|
2283
|
+
accessibilityLabel: "Export database to web inspector",
|
|
2284
|
+
accessibilityState: { busy: isExporting, disabled: exportDisabled },
|
|
2285
|
+
disabled: exportDisabled,
|
|
2286
|
+
onPress: handleExportDatabase,
|
|
2287
|
+
style: [
|
|
2288
|
+
styles3.exportButton,
|
|
2289
|
+
exportDisabled && !isExporting && styles3.exportButtonDisabled
|
|
2290
|
+
],
|
|
2291
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.exportButtonContent, children: [
|
|
2292
|
+
isExporting ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: "#f8fafc", size: "small" }) : null,
|
|
2293
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.exportLabel, children: isExporting ? "Exporting\u2026" : "Export Database" })
|
|
2294
|
+
] })
|
|
2295
|
+
}
|
|
2296
|
+
),
|
|
2297
|
+
connectionState !== "connected" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.hintText, children: "Connect to the hub before exporting." }) : null,
|
|
2298
|
+
exportState === "success" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.successText, children: "Sent to web inspector" }) : null,
|
|
2299
|
+
exportError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.errorText, children: exportError }) : null
|
|
2300
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.placeholder, children: adapterError ? "Adapter not connected" : "No adapter connected" })
|
|
2301
|
+
] })
|
|
2302
|
+
] }) })
|
|
2303
|
+
}
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
function Section({ title, children }) {
|
|
2307
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.section, children: [
|
|
2308
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.sectionTitle, children: title }),
|
|
2309
|
+
children
|
|
2310
|
+
] });
|
|
2311
|
+
}
|
|
2312
|
+
function InfoRow({
|
|
2313
|
+
label,
|
|
2314
|
+
value,
|
|
2315
|
+
mono = false
|
|
2316
|
+
}) {
|
|
2317
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.infoRow, children: [
|
|
2318
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.infoLabel, children: label }),
|
|
2319
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.infoValue, mono && styles3.mono], selectable: true, children: value })
|
|
2320
|
+
] });
|
|
2321
|
+
}
|
|
2322
|
+
var styles3 = reactNative.StyleSheet.create({
|
|
2323
|
+
overlay: {
|
|
2324
|
+
flex: 1,
|
|
2325
|
+
backgroundColor: "rgba(0, 0, 0, 0.45)",
|
|
2326
|
+
justifyContent: "flex-end"
|
|
2327
|
+
},
|
|
2328
|
+
sheet: {
|
|
2329
|
+
backgroundColor: "#ffffff",
|
|
2330
|
+
borderTopLeftRadius: 16,
|
|
2331
|
+
borderTopRightRadius: 16,
|
|
2332
|
+
maxHeight: "85%",
|
|
2333
|
+
overflow: "hidden"
|
|
2334
|
+
},
|
|
2335
|
+
scrollBody: {
|
|
2336
|
+
flexGrow: 0,
|
|
2337
|
+
flexShrink: 1
|
|
2338
|
+
},
|
|
2339
|
+
header: {
|
|
2340
|
+
flexDirection: "row",
|
|
2341
|
+
alignItems: "center",
|
|
2342
|
+
justifyContent: "space-between",
|
|
2343
|
+
paddingHorizontal: 20,
|
|
2344
|
+
paddingVertical: 16,
|
|
2345
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2346
|
+
borderBottomColor: "#e2e8f0"
|
|
2347
|
+
},
|
|
2348
|
+
title: {
|
|
2349
|
+
fontSize: 18,
|
|
2350
|
+
fontWeight: "700",
|
|
2351
|
+
color: "#0f172a"
|
|
2352
|
+
},
|
|
2353
|
+
closeButton: {
|
|
2354
|
+
fontSize: 16,
|
|
2355
|
+
color: "#2563eb",
|
|
2356
|
+
fontWeight: "600"
|
|
2357
|
+
},
|
|
2358
|
+
exportFooter: {
|
|
2359
|
+
paddingHorizontal: 20,
|
|
2360
|
+
paddingTop: 12,
|
|
2361
|
+
paddingBottom: 20,
|
|
2362
|
+
gap: 8,
|
|
2363
|
+
borderTopWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2364
|
+
borderTopColor: "#e2e8f0",
|
|
2365
|
+
backgroundColor: "#ffffff"
|
|
2366
|
+
},
|
|
2367
|
+
content: {
|
|
2368
|
+
padding: 20,
|
|
2369
|
+
gap: 20
|
|
2370
|
+
},
|
|
2371
|
+
section: {
|
|
2372
|
+
gap: 10
|
|
2373
|
+
},
|
|
2374
|
+
sectionTitle: {
|
|
2375
|
+
fontSize: 13,
|
|
2376
|
+
fontWeight: "600",
|
|
2377
|
+
color: "#64748b",
|
|
2378
|
+
textTransform: "uppercase",
|
|
2379
|
+
letterSpacing: 0.5
|
|
2380
|
+
},
|
|
2381
|
+
infoRow: {
|
|
2382
|
+
gap: 2
|
|
2383
|
+
},
|
|
2384
|
+
infoLabel: {
|
|
2385
|
+
fontSize: 12,
|
|
2386
|
+
color: "#94a3b8"
|
|
2387
|
+
},
|
|
2388
|
+
infoValue: {
|
|
2389
|
+
fontSize: 14,
|
|
2390
|
+
color: "#0f172a"
|
|
2391
|
+
},
|
|
2392
|
+
mono: {
|
|
2393
|
+
fontFamily: "monospace",
|
|
2394
|
+
fontSize: 12
|
|
2395
|
+
},
|
|
2396
|
+
fieldLabel: {
|
|
2397
|
+
fontSize: 12,
|
|
2398
|
+
color: "#64748b",
|
|
2399
|
+
marginBottom: 4
|
|
2400
|
+
},
|
|
2401
|
+
input: {
|
|
2402
|
+
borderWidth: 1,
|
|
2403
|
+
borderColor: "#cbd5e1",
|
|
2404
|
+
borderRadius: 8,
|
|
2405
|
+
paddingHorizontal: 12,
|
|
2406
|
+
paddingVertical: 10,
|
|
2407
|
+
fontSize: 14,
|
|
2408
|
+
color: "#0f172a",
|
|
2409
|
+
fontFamily: "monospace"
|
|
2410
|
+
},
|
|
2411
|
+
reconnectButton: {
|
|
2412
|
+
marginTop: 8,
|
|
2413
|
+
backgroundColor: "#1e293b",
|
|
2414
|
+
borderRadius: 8,
|
|
2415
|
+
paddingVertical: 12,
|
|
2416
|
+
alignItems: "center"
|
|
2417
|
+
},
|
|
2418
|
+
reconnectLabel: {
|
|
2419
|
+
color: "#f8fafc",
|
|
2420
|
+
fontSize: 15,
|
|
2421
|
+
fontWeight: "600"
|
|
2422
|
+
},
|
|
2423
|
+
exportButton: {
|
|
2424
|
+
backgroundColor: "#2563eb",
|
|
2425
|
+
borderRadius: 8,
|
|
2426
|
+
paddingVertical: 12,
|
|
2427
|
+
paddingHorizontal: 16,
|
|
2428
|
+
alignItems: "center",
|
|
2429
|
+
justifyContent: "center",
|
|
2430
|
+
minHeight: 44
|
|
2431
|
+
},
|
|
2432
|
+
exportButtonContent: {
|
|
2433
|
+
flexDirection: "row",
|
|
2434
|
+
alignItems: "center",
|
|
2435
|
+
justifyContent: "center",
|
|
2436
|
+
gap: 8
|
|
2437
|
+
},
|
|
2438
|
+
exportButtonDisabled: {
|
|
2439
|
+
opacity: 0.5
|
|
2440
|
+
},
|
|
2441
|
+
exportLabel: {
|
|
2442
|
+
color: "#f8fafc",
|
|
2443
|
+
fontSize: 15,
|
|
2444
|
+
fontWeight: "600"
|
|
2445
|
+
},
|
|
2446
|
+
successText: {
|
|
2447
|
+
fontSize: 13,
|
|
2448
|
+
color: "#15803d"
|
|
2449
|
+
},
|
|
2450
|
+
placeholder: {
|
|
2451
|
+
fontSize: 14,
|
|
2452
|
+
color: "#94a3b8",
|
|
2453
|
+
fontStyle: "italic"
|
|
2454
|
+
},
|
|
2455
|
+
errorText: {
|
|
2456
|
+
fontSize: 13,
|
|
2457
|
+
color: "#dc2626",
|
|
2458
|
+
marginBottom: 8
|
|
2459
|
+
},
|
|
2460
|
+
hintText: {
|
|
2461
|
+
fontSize: 12,
|
|
2462
|
+
color: "#b45309",
|
|
2463
|
+
lineHeight: 18
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
// src/utils/floatingButtonPosition.ts
|
|
2468
|
+
var FLOATING_BUTTON_SIZE = 48;
|
|
2469
|
+
var FLOATING_BUTTON_MARGIN = 16;
|
|
2470
|
+
var FLOATING_BUTTON_BOTTOM = 24;
|
|
2471
|
+
var FLOATING_BUTTON_TAP_THRESHOLD = 8;
|
|
2472
|
+
function getCornerPosition(corner, layout, bottomInset = FLOATING_BUTTON_BOTTOM) {
|
|
2473
|
+
const { windowWidth, windowHeight, buttonSize, margin } = layout;
|
|
2474
|
+
const y = windowHeight - bottomInset - buttonSize;
|
|
2475
|
+
return {
|
|
2476
|
+
x: corner === "bottom-right" ? windowWidth - margin - buttonSize : margin,
|
|
2477
|
+
y
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
function clampFloatingPosition(position, layout) {
|
|
2481
|
+
const { windowWidth, windowHeight, buttonSize, margin } = layout;
|
|
2482
|
+
return {
|
|
2483
|
+
x: Math.min(Math.max(margin, position.x), windowWidth - margin - buttonSize),
|
|
2484
|
+
y: Math.min(Math.max(margin, position.y), windowHeight - margin - buttonSize)
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
function snapFloatingPositionToEdges(position, layout) {
|
|
2488
|
+
const { windowWidth, windowHeight, buttonSize, margin } = layout;
|
|
2489
|
+
const clamped = clampFloatingPosition(position, layout);
|
|
2490
|
+
const centerX = clamped.x + buttonSize / 2;
|
|
2491
|
+
const centerY = clamped.y + buttonSize / 2;
|
|
2492
|
+
let x = centerX < windowWidth / 2 ? margin : windowWidth - margin - buttonSize;
|
|
2493
|
+
let y = clamped.y;
|
|
2494
|
+
if (centerY < windowHeight * 0.2) {
|
|
2495
|
+
y = margin;
|
|
2496
|
+
} else if (centerY > windowHeight * 0.8) {
|
|
2497
|
+
y = windowHeight - margin - buttonSize;
|
|
2498
|
+
}
|
|
2499
|
+
return { x, y };
|
|
2500
|
+
}
|
|
2501
|
+
function isFloatingButtonTap(totalMovement) {
|
|
2502
|
+
return totalMovement < FLOATING_BUTTON_TAP_THRESHOLD;
|
|
2503
|
+
}
|
|
2504
|
+
var DEFAULT_ICON_SIZE = 22;
|
|
2505
|
+
var DEFAULT_ICON_COLOR = "#f8fafc";
|
|
2506
|
+
function FloatingDevToolsButton({
|
|
2507
|
+
position = "bottom-right",
|
|
2508
|
+
iconStyle,
|
|
2509
|
+
draggable = true,
|
|
2510
|
+
snapToEdges = true,
|
|
2511
|
+
floatingPosition,
|
|
2512
|
+
onFloatingPositionChange
|
|
2513
|
+
}) {
|
|
2514
|
+
const { connectionState, openLauncher } = useDevTools();
|
|
2515
|
+
const { width: windowWidth, height: windowHeight } = reactNative.useWindowDimensions();
|
|
2516
|
+
const [internalPosition, setInternalPosition] = react.useState(null);
|
|
2517
|
+
const [dragPosition, setDragPosition] = react.useState(null);
|
|
2518
|
+
const dragOrigin = react.useRef({ x: 0, y: 0 });
|
|
2519
|
+
const hasInitialized = react.useRef(false);
|
|
2520
|
+
const onFloatingPositionChangeRef = react.useRef(onFloatingPositionChange);
|
|
2521
|
+
react.useEffect(() => {
|
|
2522
|
+
onFloatingPositionChangeRef.current = onFloatingPositionChange;
|
|
2523
|
+
}, [onFloatingPositionChange]);
|
|
2524
|
+
const layout = react.useMemo(
|
|
2525
|
+
() => ({
|
|
2526
|
+
windowWidth,
|
|
2527
|
+
windowHeight,
|
|
2528
|
+
buttonSize: FLOATING_BUTTON_SIZE,
|
|
2529
|
+
margin: FLOATING_BUTTON_MARGIN
|
|
2530
|
+
}),
|
|
2531
|
+
[windowWidth, windowHeight]
|
|
2532
|
+
);
|
|
2533
|
+
const committedPosition = floatingPosition ?? internalPosition;
|
|
2534
|
+
const renderedPosition = dragPosition ?? committedPosition;
|
|
2535
|
+
const commitPosition = react.useCallback(
|
|
2536
|
+
(next) => {
|
|
2537
|
+
const clamped = clampFloatingPosition(next, layout);
|
|
2538
|
+
if (!floatingPosition) {
|
|
2539
|
+
setInternalPosition(clamped);
|
|
2540
|
+
}
|
|
2541
|
+
onFloatingPositionChangeRef.current?.(clamped);
|
|
2542
|
+
return clamped;
|
|
2543
|
+
},
|
|
2544
|
+
[floatingPosition, layout]
|
|
2545
|
+
);
|
|
2546
|
+
react.useEffect(() => {
|
|
2547
|
+
if (windowWidth <= 0 || windowHeight <= 0) {
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
if (floatingPosition) {
|
|
2551
|
+
hasInitialized.current = true;
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (!hasInitialized.current) {
|
|
2555
|
+
setInternalPosition(getCornerPosition(position, layout));
|
|
2556
|
+
hasInitialized.current = true;
|
|
2557
|
+
}
|
|
2558
|
+
}, [floatingPosition, layout, position, windowHeight, windowWidth]);
|
|
2559
|
+
react.useEffect(() => {
|
|
2560
|
+
if (!committedPosition || windowWidth <= 0 || windowHeight <= 0) {
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
const clamped = clampFloatingPosition(committedPosition, layout);
|
|
2564
|
+
if (clamped.x !== committedPosition.x || clamped.y !== committedPosition.y) {
|
|
2565
|
+
commitPosition(clamped);
|
|
2566
|
+
}
|
|
2567
|
+
}, [commitPosition, committedPosition, layout, windowHeight, windowWidth]);
|
|
2568
|
+
const panResponder = react.useMemo(
|
|
2569
|
+
() => reactNative.PanResponder.create({
|
|
2570
|
+
onStartShouldSetPanResponder: () => draggable,
|
|
2571
|
+
onMoveShouldSetPanResponder: (_, gesture) => draggable && (Math.abs(gesture.dx) > 2 || Math.abs(gesture.dy) > 2),
|
|
2572
|
+
onPanResponderGrant: () => {
|
|
2573
|
+
if (!committedPosition) {
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
dragOrigin.current = committedPosition;
|
|
2577
|
+
},
|
|
2578
|
+
onPanResponderMove: (_, gesture) => {
|
|
2579
|
+
setDragPosition(
|
|
2580
|
+
clampFloatingPosition(
|
|
2581
|
+
{
|
|
2582
|
+
x: dragOrigin.current.x + gesture.dx,
|
|
2583
|
+
y: dragOrigin.current.y + gesture.dy
|
|
2584
|
+
},
|
|
2585
|
+
layout
|
|
2586
|
+
)
|
|
2587
|
+
);
|
|
2588
|
+
},
|
|
2589
|
+
onPanResponderRelease: (_, gesture) => {
|
|
2590
|
+
setDragPosition(null);
|
|
2591
|
+
const movement = Math.hypot(gesture.dx, gesture.dy);
|
|
2592
|
+
if (isFloatingButtonTap(movement)) {
|
|
2593
|
+
openLauncher();
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const raw = {
|
|
2597
|
+
x: dragOrigin.current.x + gesture.dx,
|
|
2598
|
+
y: dragOrigin.current.y + gesture.dy
|
|
2599
|
+
};
|
|
2600
|
+
const next = snapToEdges ? snapFloatingPositionToEdges(raw, layout) : clampFloatingPosition(raw, layout);
|
|
2601
|
+
commitPosition(next);
|
|
2602
|
+
},
|
|
2603
|
+
onPanResponderTerminate: () => {
|
|
2604
|
+
setDragPosition(null);
|
|
2605
|
+
}
|
|
2606
|
+
}),
|
|
2607
|
+
[commitPosition, committedPosition, draggable, layout, openLauncher, snapToEdges]
|
|
2608
|
+
);
|
|
2609
|
+
if (!renderedPosition) {
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
const buttonContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2613
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2614
|
+
MaterialCommunityIcons__default.default,
|
|
2615
|
+
{
|
|
2616
|
+
color: DEFAULT_ICON_COLOR,
|
|
2617
|
+
name: "database",
|
|
2618
|
+
size: DEFAULT_ICON_SIZE,
|
|
2619
|
+
style: iconStyle
|
|
2620
|
+
}
|
|
2621
|
+
),
|
|
2622
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2623
|
+
reactNative.View,
|
|
2624
|
+
{
|
|
2625
|
+
style: [styles4.statusDot, { backgroundColor: getConnectionDotColor(connectionState) }]
|
|
2626
|
+
}
|
|
2627
|
+
)
|
|
2628
|
+
] });
|
|
2629
|
+
if (!draggable) {
|
|
2630
|
+
const positionStyle = position === "bottom-left" ? styles4.bottomLeft : styles4.bottomRight;
|
|
2631
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.fixedContainer, positionStyle], pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2632
|
+
reactNative.Pressable,
|
|
2633
|
+
{
|
|
2634
|
+
accessibilityLabel: "Open Database DevTools",
|
|
2635
|
+
accessibilityRole: "button",
|
|
2636
|
+
onPress: openLauncher,
|
|
2637
|
+
style: ({ pressed }) => [styles4.button, pressed && styles4.buttonPressed],
|
|
2638
|
+
children: buttonContent
|
|
2639
|
+
}
|
|
2640
|
+
) });
|
|
2641
|
+
}
|
|
2642
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2643
|
+
reactNative.View,
|
|
2644
|
+
{
|
|
2645
|
+
pointerEvents: "box-none",
|
|
2646
|
+
style: [
|
|
2647
|
+
styles4.draggableContainer,
|
|
2648
|
+
{
|
|
2649
|
+
transform: [
|
|
2650
|
+
{ translateX: renderedPosition.x },
|
|
2651
|
+
{ translateY: renderedPosition.y }
|
|
2652
|
+
]
|
|
2653
|
+
}
|
|
2654
|
+
],
|
|
2655
|
+
...panResponder.panHandlers,
|
|
2656
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2657
|
+
reactNative.View,
|
|
2658
|
+
{
|
|
2659
|
+
accessibilityHint: "Drag to move. Tap to open DevTools launcher.",
|
|
2660
|
+
accessibilityLabel: "Open Database DevTools",
|
|
2661
|
+
accessibilityRole: "button",
|
|
2662
|
+
style: [styles4.button, dragPosition && styles4.buttonDragging],
|
|
2663
|
+
children: buttonContent
|
|
2664
|
+
}
|
|
2665
|
+
)
|
|
2666
|
+
}
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
var styles4 = reactNative.StyleSheet.create({
|
|
2670
|
+
fixedContainer: {
|
|
2671
|
+
position: "absolute",
|
|
2672
|
+
bottom: 24,
|
|
2673
|
+
zIndex: 9999
|
|
2674
|
+
},
|
|
2675
|
+
bottomRight: {
|
|
2676
|
+
right: 16
|
|
2677
|
+
},
|
|
2678
|
+
bottomLeft: {
|
|
2679
|
+
left: 16
|
|
2680
|
+
},
|
|
2681
|
+
draggableContainer: {
|
|
2682
|
+
position: "absolute",
|
|
2683
|
+
top: 0,
|
|
2684
|
+
left: 0,
|
|
2685
|
+
zIndex: 9999
|
|
2686
|
+
},
|
|
2687
|
+
button: {
|
|
2688
|
+
width: FLOATING_BUTTON_SIZE,
|
|
2689
|
+
height: FLOATING_BUTTON_SIZE,
|
|
2690
|
+
borderRadius: FLOATING_BUTTON_SIZE / 2,
|
|
2691
|
+
backgroundColor: "#1e293b",
|
|
2692
|
+
alignItems: "center",
|
|
2693
|
+
justifyContent: "center",
|
|
2694
|
+
shadowColor: "#000",
|
|
2695
|
+
shadowOffset: { width: 0, height: 2 },
|
|
2696
|
+
shadowOpacity: 0.25,
|
|
2697
|
+
shadowRadius: 4,
|
|
2698
|
+
elevation: 6
|
|
2699
|
+
},
|
|
2700
|
+
buttonPressed: {
|
|
2701
|
+
opacity: 0.85
|
|
2702
|
+
},
|
|
2703
|
+
buttonDragging: {
|
|
2704
|
+
opacity: 0.92,
|
|
2705
|
+
transform: [{ scale: 1.04 }]
|
|
2706
|
+
},
|
|
2707
|
+
statusDot: {
|
|
2708
|
+
position: "absolute",
|
|
2709
|
+
top: 2,
|
|
2710
|
+
right: 2,
|
|
2711
|
+
width: 10,
|
|
2712
|
+
height: 10,
|
|
2713
|
+
borderRadius: 5,
|
|
2714
|
+
borderWidth: 2,
|
|
2715
|
+
borderColor: "#1e293b"
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
var explorerColors = {
|
|
2719
|
+
background: "#ffffff",
|
|
2720
|
+
surface: "#f8fafc",
|
|
2721
|
+
border: "#e2e8f0",
|
|
2722
|
+
text: "#0f172a",
|
|
2723
|
+
muted: "#64748b",
|
|
2724
|
+
subtle: "#94a3b8",
|
|
2725
|
+
accent: "#2563eb",
|
|
2726
|
+
accentSoft: "#dbeafe",
|
|
2727
|
+
error: "#dc2626"};
|
|
2728
|
+
var explorerStyles = reactNative.StyleSheet.create({
|
|
2729
|
+
fullScreen: {
|
|
2730
|
+
flex: 1,
|
|
2731
|
+
backgroundColor: explorerColors.background
|
|
2732
|
+
},
|
|
2733
|
+
header: {
|
|
2734
|
+
flexDirection: "row",
|
|
2735
|
+
alignItems: "center",
|
|
2736
|
+
justifyContent: "space-between",
|
|
2737
|
+
paddingHorizontal: 16,
|
|
2738
|
+
paddingVertical: 14,
|
|
2739
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2740
|
+
borderBottomColor: explorerColors.border,
|
|
2741
|
+
backgroundColor: explorerColors.background
|
|
2742
|
+
},
|
|
2743
|
+
headerTitle: {
|
|
2744
|
+
fontSize: 17,
|
|
2745
|
+
fontWeight: "700",
|
|
2746
|
+
color: explorerColors.text
|
|
2747
|
+
},
|
|
2748
|
+
headerAction: {
|
|
2749
|
+
fontSize: 16,
|
|
2750
|
+
color: explorerColors.accent,
|
|
2751
|
+
fontWeight: "600"
|
|
2752
|
+
},
|
|
2753
|
+
tabRow: {
|
|
2754
|
+
flexDirection: "row",
|
|
2755
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2756
|
+
borderBottomColor: explorerColors.border,
|
|
2757
|
+
backgroundColor: explorerColors.surface
|
|
2758
|
+
},
|
|
2759
|
+
tab: {
|
|
2760
|
+
flex: 1,
|
|
2761
|
+
paddingVertical: 12,
|
|
2762
|
+
alignItems: "center",
|
|
2763
|
+
borderBottomWidth: 2,
|
|
2764
|
+
borderBottomColor: "transparent"
|
|
2765
|
+
},
|
|
2766
|
+
tabActive: {
|
|
2767
|
+
borderBottomColor: explorerColors.accent
|
|
2768
|
+
},
|
|
2769
|
+
tabLabel: {
|
|
2770
|
+
fontSize: 14,
|
|
2771
|
+
color: explorerColors.muted,
|
|
2772
|
+
fontWeight: "500"
|
|
2773
|
+
},
|
|
2774
|
+
tabLabelActive: {
|
|
2775
|
+
color: explorerColors.accent,
|
|
2776
|
+
fontWeight: "700"
|
|
2777
|
+
},
|
|
2778
|
+
content: {
|
|
2779
|
+
flex: 1,
|
|
2780
|
+
padding: 16
|
|
2781
|
+
},
|
|
2782
|
+
sectionTitle: {
|
|
2783
|
+
fontSize: 12,
|
|
2784
|
+
fontWeight: "600",
|
|
2785
|
+
color: explorerColors.muted,
|
|
2786
|
+
textTransform: "uppercase",
|
|
2787
|
+
letterSpacing: 0.5,
|
|
2788
|
+
marginBottom: 8
|
|
2789
|
+
},
|
|
2790
|
+
infoRow: {
|
|
2791
|
+
marginBottom: 10
|
|
2792
|
+
},
|
|
2793
|
+
infoLabel: {
|
|
2794
|
+
fontSize: 12,
|
|
2795
|
+
color: explorerColors.subtle,
|
|
2796
|
+
marginBottom: 2
|
|
2797
|
+
},
|
|
2798
|
+
infoValue: {
|
|
2799
|
+
fontSize: 14,
|
|
2800
|
+
color: explorerColors.text
|
|
2801
|
+
},
|
|
2802
|
+
mono: {
|
|
2803
|
+
fontFamily: "monospace",
|
|
2804
|
+
fontSize: 12
|
|
2805
|
+
},
|
|
2806
|
+
listItem: {
|
|
2807
|
+
flexDirection: "row",
|
|
2808
|
+
alignItems: "center",
|
|
2809
|
+
justifyContent: "space-between",
|
|
2810
|
+
paddingVertical: 12,
|
|
2811
|
+
paddingHorizontal: 12,
|
|
2812
|
+
borderRadius: 8,
|
|
2813
|
+
backgroundColor: explorerColors.surface,
|
|
2814
|
+
marginBottom: 8,
|
|
2815
|
+
borderWidth: 1,
|
|
2816
|
+
borderColor: explorerColors.border
|
|
2817
|
+
},
|
|
2818
|
+
listItemTitle: {
|
|
2819
|
+
fontSize: 15,
|
|
2820
|
+
fontWeight: "600",
|
|
2821
|
+
color: explorerColors.text,
|
|
2822
|
+
fontFamily: "monospace"
|
|
2823
|
+
},
|
|
2824
|
+
listItemMeta: {
|
|
2825
|
+
fontSize: 12,
|
|
2826
|
+
color: explorerColors.muted
|
|
2827
|
+
},
|
|
2828
|
+
searchInput: {
|
|
2829
|
+
borderWidth: 1,
|
|
2830
|
+
borderColor: explorerColors.border,
|
|
2831
|
+
borderRadius: 8,
|
|
2832
|
+
paddingHorizontal: 12,
|
|
2833
|
+
paddingVertical: 10,
|
|
2834
|
+
fontSize: 14,
|
|
2835
|
+
color: explorerColors.text,
|
|
2836
|
+
backgroundColor: explorerColors.background,
|
|
2837
|
+
marginBottom: 12
|
|
2838
|
+
},
|
|
2839
|
+
primaryButton: {
|
|
2840
|
+
backgroundColor: explorerColors.accent,
|
|
2841
|
+
borderRadius: 8,
|
|
2842
|
+
paddingVertical: 12,
|
|
2843
|
+
alignItems: "center",
|
|
2844
|
+
justifyContent: "center",
|
|
2845
|
+
minHeight: 44
|
|
2846
|
+
},
|
|
2847
|
+
primaryButtonDisabled: {
|
|
2848
|
+
opacity: 0.5
|
|
2849
|
+
},
|
|
2850
|
+
primaryButtonLabel: {
|
|
2851
|
+
color: "#f8fafc",
|
|
2852
|
+
fontSize: 15,
|
|
2853
|
+
fontWeight: "600"
|
|
2854
|
+
},
|
|
2855
|
+
secondaryButton: {
|
|
2856
|
+
backgroundColor: "#1e293b",
|
|
2857
|
+
borderRadius: 8,
|
|
2858
|
+
paddingVertical: 12,
|
|
2859
|
+
alignItems: "center",
|
|
2860
|
+
justifyContent: "center",
|
|
2861
|
+
minHeight: 44
|
|
2862
|
+
},
|
|
2863
|
+
secondaryButtonLabel: {
|
|
2864
|
+
color: "#f8fafc",
|
|
2865
|
+
fontSize: 15,
|
|
2866
|
+
fontWeight: "600"
|
|
2867
|
+
},
|
|
2868
|
+
ghostButton: {
|
|
2869
|
+
borderWidth: 1,
|
|
2870
|
+
borderColor: explorerColors.border,
|
|
2871
|
+
borderRadius: 8,
|
|
2872
|
+
paddingVertical: 12,
|
|
2873
|
+
alignItems: "center",
|
|
2874
|
+
justifyContent: "center",
|
|
2875
|
+
minHeight: 44,
|
|
2876
|
+
backgroundColor: explorerColors.background
|
|
2877
|
+
},
|
|
2878
|
+
ghostButtonLabel: {
|
|
2879
|
+
color: explorerColors.text,
|
|
2880
|
+
fontSize: 15,
|
|
2881
|
+
fontWeight: "600"
|
|
2882
|
+
},
|
|
2883
|
+
errorText: {
|
|
2884
|
+
fontSize: 13,
|
|
2885
|
+
color: explorerColors.error,
|
|
2886
|
+
marginTop: 8
|
|
2887
|
+
},
|
|
2888
|
+
hintText: {
|
|
2889
|
+
fontSize: 12,
|
|
2890
|
+
color: explorerColors.muted,
|
|
2891
|
+
marginTop: 8,
|
|
2892
|
+
lineHeight: 18
|
|
2893
|
+
},
|
|
2894
|
+
placeholder: {
|
|
2895
|
+
fontSize: 14,
|
|
2896
|
+
color: explorerColors.subtle,
|
|
2897
|
+
fontStyle: "italic",
|
|
2898
|
+
textAlign: "center",
|
|
2899
|
+
marginTop: 24
|
|
2900
|
+
},
|
|
2901
|
+
sqlInput: {
|
|
2902
|
+
minHeight: 120,
|
|
2903
|
+
borderWidth: 1,
|
|
2904
|
+
borderColor: explorerColors.border,
|
|
2905
|
+
borderRadius: 8,
|
|
2906
|
+
paddingHorizontal: 12,
|
|
2907
|
+
paddingVertical: 10,
|
|
2908
|
+
fontSize: 13,
|
|
2909
|
+
fontFamily: "monospace",
|
|
2910
|
+
color: explorerColors.text,
|
|
2911
|
+
textAlignVertical: "top",
|
|
2912
|
+
backgroundColor: explorerColors.surface
|
|
2913
|
+
},
|
|
2914
|
+
sqlTabRoot: {
|
|
2915
|
+
flex: 1,
|
|
2916
|
+
minHeight: 0
|
|
2917
|
+
},
|
|
2918
|
+
sqlQueryPane: {
|
|
2919
|
+
flexShrink: 0,
|
|
2920
|
+
paddingHorizontal: 16,
|
|
2921
|
+
paddingTop: 16,
|
|
2922
|
+
paddingBottom: 12,
|
|
2923
|
+
gap: 12,
|
|
2924
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2925
|
+
borderBottomColor: explorerColors.border,
|
|
2926
|
+
backgroundColor: explorerColors.background
|
|
2927
|
+
},
|
|
2928
|
+
sqlInputCompact: {
|
|
2929
|
+
minHeight: 96,
|
|
2930
|
+
maxHeight: 120,
|
|
2931
|
+
borderWidth: 1,
|
|
2932
|
+
borderColor: explorerColors.border,
|
|
2933
|
+
borderRadius: 8,
|
|
2934
|
+
paddingHorizontal: 12,
|
|
2935
|
+
paddingVertical: 10,
|
|
2936
|
+
fontSize: 13,
|
|
2937
|
+
fontFamily: "monospace",
|
|
2938
|
+
color: explorerColors.text,
|
|
2939
|
+
textAlignVertical: "top",
|
|
2940
|
+
backgroundColor: explorerColors.surface
|
|
2941
|
+
},
|
|
2942
|
+
sqlResultsPane: {
|
|
2943
|
+
flex: 1,
|
|
2944
|
+
minHeight: 0,
|
|
2945
|
+
paddingHorizontal: 16,
|
|
2946
|
+
paddingTop: 8,
|
|
2947
|
+
paddingBottom: 16
|
|
2948
|
+
},
|
|
2949
|
+
sqlResultsEmpty: {
|
|
2950
|
+
flex: 1,
|
|
2951
|
+
justifyContent: "center",
|
|
2952
|
+
paddingHorizontal: 16
|
|
2953
|
+
},
|
|
2954
|
+
subTabRow: {
|
|
2955
|
+
flexDirection: "row",
|
|
2956
|
+
gap: 8,
|
|
2957
|
+
marginBottom: 12
|
|
2958
|
+
},
|
|
2959
|
+
subTab: {
|
|
2960
|
+
paddingHorizontal: 12,
|
|
2961
|
+
paddingVertical: 6,
|
|
2962
|
+
borderRadius: 999,
|
|
2963
|
+
borderWidth: 1,
|
|
2964
|
+
borderColor: explorerColors.border,
|
|
2965
|
+
backgroundColor: explorerColors.background
|
|
2966
|
+
},
|
|
2967
|
+
subTabActive: {
|
|
2968
|
+
borderColor: explorerColors.accent,
|
|
2969
|
+
backgroundColor: explorerColors.accentSoft
|
|
2970
|
+
},
|
|
2971
|
+
subTabLabel: {
|
|
2972
|
+
fontSize: 13,
|
|
2973
|
+
color: explorerColors.muted
|
|
2974
|
+
},
|
|
2975
|
+
subTabLabelActive: {
|
|
2976
|
+
color: explorerColors.accent,
|
|
2977
|
+
fontWeight: "600"
|
|
2978
|
+
},
|
|
2979
|
+
paginationRow: {
|
|
2980
|
+
flexDirection: "row",
|
|
2981
|
+
alignItems: "center",
|
|
2982
|
+
justifyContent: "space-between",
|
|
2983
|
+
paddingVertical: 12,
|
|
2984
|
+
borderTopWidth: reactNative.StyleSheet.hairlineWidth,
|
|
2985
|
+
borderTopColor: explorerColors.border
|
|
2986
|
+
},
|
|
2987
|
+
paginationLabel: {
|
|
2988
|
+
fontSize: 12,
|
|
2989
|
+
color: explorerColors.muted
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
var tableStyles = reactNative.StyleSheet.create({
|
|
2993
|
+
viewRoot: {
|
|
2994
|
+
flex: 1,
|
|
2995
|
+
minHeight: 0
|
|
2996
|
+
},
|
|
2997
|
+
viewToolbar: {
|
|
2998
|
+
flexDirection: "row",
|
|
2999
|
+
alignItems: "center",
|
|
3000
|
+
justifyContent: "space-between",
|
|
3001
|
+
gap: 8,
|
|
3002
|
+
marginBottom: 8,
|
|
3003
|
+
flexWrap: "wrap"
|
|
3004
|
+
},
|
|
3005
|
+
viewMeta: {
|
|
3006
|
+
flex: 1,
|
|
3007
|
+
fontSize: 12,
|
|
3008
|
+
color: explorerColors.muted
|
|
3009
|
+
},
|
|
3010
|
+
viewToggle: {
|
|
3011
|
+
flexDirection: "row",
|
|
3012
|
+
borderWidth: 1,
|
|
3013
|
+
borderColor: explorerColors.border,
|
|
3014
|
+
borderRadius: 8,
|
|
3015
|
+
overflow: "hidden"
|
|
3016
|
+
},
|
|
3017
|
+
viewToggleButton: {
|
|
3018
|
+
paddingHorizontal: 10,
|
|
3019
|
+
paddingVertical: 6,
|
|
3020
|
+
backgroundColor: explorerColors.background
|
|
3021
|
+
},
|
|
3022
|
+
viewToggleButtonActive: {
|
|
3023
|
+
backgroundColor: explorerColors.accentSoft
|
|
3024
|
+
},
|
|
3025
|
+
viewToggleLabel: {
|
|
3026
|
+
fontSize: 12,
|
|
3027
|
+
fontWeight: "600",
|
|
3028
|
+
color: explorerColors.muted
|
|
3029
|
+
},
|
|
3030
|
+
viewToggleLabelActive: {
|
|
3031
|
+
color: explorerColors.accent
|
|
3032
|
+
},
|
|
3033
|
+
footerHint: {
|
|
3034
|
+
flexShrink: 0,
|
|
3035
|
+
marginTop: 8,
|
|
3036
|
+
fontSize: 11,
|
|
3037
|
+
color: explorerColors.subtle,
|
|
3038
|
+
textAlign: "center"
|
|
3039
|
+
},
|
|
3040
|
+
tableContainer: {
|
|
3041
|
+
flex: 1,
|
|
3042
|
+
minHeight: 0,
|
|
3043
|
+
borderWidth: 1,
|
|
3044
|
+
borderColor: explorerColors.border,
|
|
3045
|
+
borderRadius: 8,
|
|
3046
|
+
overflow: "hidden",
|
|
3047
|
+
backgroundColor: explorerColors.background
|
|
3048
|
+
},
|
|
3049
|
+
horizontalScroll: {
|
|
3050
|
+
flex: 1
|
|
3051
|
+
},
|
|
3052
|
+
scrollHint: {
|
|
3053
|
+
paddingHorizontal: 10,
|
|
3054
|
+
paddingVertical: 6,
|
|
3055
|
+
fontSize: 11,
|
|
3056
|
+
color: explorerColors.muted,
|
|
3057
|
+
backgroundColor: explorerColors.surface,
|
|
3058
|
+
borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
|
|
3059
|
+
borderBottomColor: explorerColors.border
|
|
3060
|
+
},
|
|
3061
|
+
headerRow: {
|
|
3062
|
+
flexDirection: "row",
|
|
3063
|
+
backgroundColor: "#f1f5f9",
|
|
3064
|
+
borderBottomWidth: 1,
|
|
3065
|
+
borderBottomColor: explorerColors.border
|
|
3066
|
+
},
|
|
3067
|
+
headerCell: {
|
|
3068
|
+
backgroundColor: "#f1f5f9"
|
|
3069
|
+
},
|
|
3070
|
+
headerText: {
|
|
3071
|
+
fontSize: 12,
|
|
3072
|
+
fontWeight: "700",
|
|
3073
|
+
color: explorerColors.text,
|
|
3074
|
+
fontFamily: "monospace"
|
|
3075
|
+
},
|
|
3076
|
+
bodyList: {
|
|
3077
|
+
flex: 1
|
|
3078
|
+
},
|
|
3079
|
+
dataRow: {
|
|
3080
|
+
flexDirection: "row",
|
|
3081
|
+
backgroundColor: explorerColors.background
|
|
3082
|
+
},
|
|
3083
|
+
dataRowAlt: {
|
|
3084
|
+
backgroundColor: explorerColors.surface
|
|
3085
|
+
},
|
|
3086
|
+
cell: {
|
|
3087
|
+
paddingHorizontal: 10,
|
|
3088
|
+
paddingVertical: 8,
|
|
3089
|
+
borderRightWidth: 1,
|
|
3090
|
+
borderBottomWidth: 1,
|
|
3091
|
+
borderColor: explorerColors.border,
|
|
3092
|
+
justifyContent: "center"
|
|
3093
|
+
},
|
|
3094
|
+
cellText: {
|
|
3095
|
+
fontSize: 13,
|
|
3096
|
+
color: explorerColors.text
|
|
3097
|
+
},
|
|
3098
|
+
cellTextMono: {
|
|
3099
|
+
fontFamily: "monospace",
|
|
3100
|
+
fontSize: 12
|
|
3101
|
+
},
|
|
3102
|
+
cellNull: {
|
|
3103
|
+
color: explorerColors.subtle,
|
|
3104
|
+
fontStyle: "italic"
|
|
3105
|
+
},
|
|
3106
|
+
copiedHint: {
|
|
3107
|
+
marginTop: 2,
|
|
3108
|
+
fontSize: 10,
|
|
3109
|
+
color: explorerColors.accent,
|
|
3110
|
+
fontWeight: "600"
|
|
3111
|
+
},
|
|
3112
|
+
cardList: {
|
|
3113
|
+
paddingBottom: 8,
|
|
3114
|
+
gap: 8
|
|
3115
|
+
},
|
|
3116
|
+
card: {
|
|
3117
|
+
borderWidth: 1,
|
|
3118
|
+
borderColor: explorerColors.border,
|
|
3119
|
+
borderRadius: 8,
|
|
3120
|
+
backgroundColor: explorerColors.surface,
|
|
3121
|
+
padding: 12,
|
|
3122
|
+
gap: 10
|
|
3123
|
+
},
|
|
3124
|
+
cardField: {
|
|
3125
|
+
gap: 2
|
|
3126
|
+
},
|
|
3127
|
+
cardLabel: {
|
|
3128
|
+
fontSize: 11,
|
|
3129
|
+
fontWeight: "600",
|
|
3130
|
+
color: explorerColors.muted,
|
|
3131
|
+
textTransform: "uppercase",
|
|
3132
|
+
letterSpacing: 0.4
|
|
3133
|
+
},
|
|
3134
|
+
cardValue: {
|
|
3135
|
+
fontSize: 14,
|
|
3136
|
+
color: explorerColors.text
|
|
3137
|
+
},
|
|
3138
|
+
cardValueMono: {
|
|
3139
|
+
fontFamily: "monospace",
|
|
3140
|
+
fontSize: 13
|
|
3141
|
+
},
|
|
3142
|
+
detailBody: {
|
|
3143
|
+
flex: 1,
|
|
3144
|
+
minHeight: 0,
|
|
3145
|
+
paddingHorizontal: 16
|
|
3146
|
+
},
|
|
3147
|
+
detailDataPane: {
|
|
3148
|
+
flex: 1,
|
|
3149
|
+
minHeight: 0
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
function formatBytes(bytes) {
|
|
3153
|
+
if (bytes < 1024) {
|
|
3154
|
+
return `${bytes} B`;
|
|
3155
|
+
}
|
|
3156
|
+
if (bytes < 1024 * 1024) {
|
|
3157
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3158
|
+
}
|
|
3159
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3160
|
+
}
|
|
3161
|
+
function ExplorerOverviewTab({
|
|
3162
|
+
info,
|
|
3163
|
+
loading,
|
|
3164
|
+
error,
|
|
3165
|
+
onOpenTables,
|
|
3166
|
+
onOpenSql
|
|
3167
|
+
}) {
|
|
3168
|
+
if (loading) {
|
|
3169
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "Loading database info\u2026" });
|
|
3170
|
+
}
|
|
3171
|
+
if (error) {
|
|
3172
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.errorText, children: error });
|
|
3173
|
+
}
|
|
3174
|
+
if (!info) {
|
|
3175
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "No database information available." });
|
|
3176
|
+
}
|
|
3177
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.ScrollView, { contentContainerStyle: explorerStyles.content, children: [
|
|
3178
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.sectionTitle, children: "Database" }),
|
|
3179
|
+
/* @__PURE__ */ jsxRuntime.jsx(Info, { label: "Name", value: info.name }),
|
|
3180
|
+
/* @__PURE__ */ jsxRuntime.jsx(Info, { label: "Path", value: info.path, mono: true }),
|
|
3181
|
+
/* @__PURE__ */ jsxRuntime.jsx(Info, { label: "SQLite version", value: info.sqliteVersion }),
|
|
3182
|
+
/* @__PURE__ */ jsxRuntime.jsx(Info, { label: "Tables", value: String(info.tableCount) }),
|
|
3183
|
+
/* @__PURE__ */ jsxRuntime.jsx(Info, { label: "Estimated size", value: formatBytes(info.estimatedSizeBytes) }),
|
|
3184
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3185
|
+
Info,
|
|
3186
|
+
{
|
|
3187
|
+
label: "Pages",
|
|
3188
|
+
value: `${info.pageCount.toLocaleString()} \xD7 ${info.pageSize} bytes`
|
|
3189
|
+
}
|
|
3190
|
+
),
|
|
3191
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { height: 16 } }),
|
|
3192
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.sectionTitle, children: "Quick actions" }),
|
|
3193
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: 8 }, children: [
|
|
3194
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActionButton, { label: "Browse tables", onPress: onOpenTables }),
|
|
3195
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActionButton, { label: "Run SQL query", onPress: onOpenSql })
|
|
3196
|
+
] })
|
|
3197
|
+
] });
|
|
3198
|
+
}
|
|
3199
|
+
function Info({ label, value, mono = false }) {
|
|
3200
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.infoRow, children: [
|
|
3201
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.infoLabel, children: label }),
|
|
3202
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [explorerStyles.infoValue, mono && explorerStyles.mono], selectable: true, children: value })
|
|
3203
|
+
] });
|
|
3204
|
+
}
|
|
3205
|
+
function ActionButton({ label, onPress }) {
|
|
3206
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress, style: explorerStyles.ghostButton, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.ghostButtonLabel, children: label }) });
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/mobile/constants.ts
|
|
3210
|
+
var DEFAULT_MOBILE_SQL = "";
|
|
3211
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
3212
|
+
|
|
3213
|
+
// src/mobile/computeColumnWidths.ts
|
|
3214
|
+
var CHAR_WIDTH = 7.5;
|
|
3215
|
+
var CELL_PADDING = 24;
|
|
3216
|
+
var MIN_COLUMN_WIDTH = 72;
|
|
3217
|
+
var MAX_COLUMN_WIDTH = 280;
|
|
3218
|
+
var SAMPLE_ROW_LIMIT = 20;
|
|
3219
|
+
function hintMinWidth(columnName) {
|
|
3220
|
+
const lower = columnName.toLowerCase();
|
|
3221
|
+
if (lower === "id" || lower.endsWith("_id")) {
|
|
3222
|
+
return 80;
|
|
3223
|
+
}
|
|
3224
|
+
if (lower.includes("email")) {
|
|
3225
|
+
return 168;
|
|
3226
|
+
}
|
|
3227
|
+
if (lower.includes("created_at") || lower.includes("updated_at") || lower.endsWith("_at")) {
|
|
3228
|
+
return 188;
|
|
3229
|
+
}
|
|
3230
|
+
if (lower === "name" || lower.includes("title")) {
|
|
3231
|
+
return 128;
|
|
3232
|
+
}
|
|
3233
|
+
if (lower === "status" || lower === "type") {
|
|
3234
|
+
return 96;
|
|
3235
|
+
}
|
|
3236
|
+
return MIN_COLUMN_WIDTH;
|
|
3237
|
+
}
|
|
3238
|
+
function measureText(text) {
|
|
3239
|
+
return Math.ceil(text.length * CHAR_WIDTH) + CELL_PADDING;
|
|
3240
|
+
}
|
|
3241
|
+
function formatCellValue(value) {
|
|
3242
|
+
if (value === null) {
|
|
3243
|
+
return "NULL";
|
|
3244
|
+
}
|
|
3245
|
+
return String(value);
|
|
3246
|
+
}
|
|
3247
|
+
function computeColumnWidths(columns, rows) {
|
|
3248
|
+
const sampleRows = rows.slice(0, SAMPLE_ROW_LIMIT);
|
|
3249
|
+
return columns.map((column, columnIndex) => {
|
|
3250
|
+
let maxWidth = measureText(column);
|
|
3251
|
+
for (const row of sampleRows) {
|
|
3252
|
+
const cell = formatCellValue(row[columnIndex] ?? null);
|
|
3253
|
+
maxWidth = Math.max(maxWidth, measureText(cell));
|
|
3254
|
+
}
|
|
3255
|
+
maxWidth = Math.max(maxWidth, hintMinWidth(column));
|
|
3256
|
+
return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, maxWidth));
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
function totalTableWidth(columnWidths) {
|
|
3260
|
+
return columnWidths.reduce((sum, width) => sum + width, 0);
|
|
3261
|
+
}
|
|
3262
|
+
function isMonospaceColumn(columnName) {
|
|
3263
|
+
const lower = columnName.toLowerCase();
|
|
3264
|
+
return lower === "id" || lower.endsWith("_id") || lower.includes("created_at") || lower.includes("updated_at") || lower.endsWith("_at") || lower.includes("date") || lower.includes("time");
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// src/mobile/copyToClipboard.ts
|
|
3268
|
+
async function copyToClipboard(_text) {
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
function formatValue(value) {
|
|
3272
|
+
return value === null ? "NULL" : String(value);
|
|
3273
|
+
}
|
|
3274
|
+
function MobileDataCardList({ columns, rows }) {
|
|
3275
|
+
const [copiedKey, setCopiedKey] = react.useState(null);
|
|
3276
|
+
const copyValue = async (value, key) => {
|
|
3277
|
+
await copyToClipboard();
|
|
3278
|
+
setCopiedKey(key);
|
|
3279
|
+
setTimeout(() => setCopiedKey(null), 1500);
|
|
3280
|
+
};
|
|
3281
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3282
|
+
reactNative.FlatList,
|
|
3283
|
+
{
|
|
3284
|
+
data: rows,
|
|
3285
|
+
keyExtractor: (_, index) => String(index),
|
|
3286
|
+
contentContainerStyle: tableStyles.cardList,
|
|
3287
|
+
style: tableStyles.bodyList,
|
|
3288
|
+
renderItem: ({ item: row, index: rowIndex }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: tableStyles.card, children: columns.map((column, columnIndex) => {
|
|
3289
|
+
const value = formatValue(row[columnIndex] ?? null);
|
|
3290
|
+
const key = `${rowIndex}-${column}`;
|
|
3291
|
+
const isNull = row[columnIndex] === null;
|
|
3292
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3293
|
+
reactNative.Pressable,
|
|
3294
|
+
{
|
|
3295
|
+
onPress: () => void copyValue(value, key),
|
|
3296
|
+
style: tableStyles.cardField,
|
|
3297
|
+
children: [
|
|
3298
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.cardLabel, children: column }),
|
|
3299
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3300
|
+
reactNative.Text,
|
|
3301
|
+
{
|
|
3302
|
+
style: [
|
|
3303
|
+
tableStyles.cardValue,
|
|
3304
|
+
isMonospaceColumn(column) && tableStyles.cardValueMono,
|
|
3305
|
+
isNull && tableStyles.cellNull
|
|
3306
|
+
],
|
|
3307
|
+
children: value
|
|
3308
|
+
}
|
|
3309
|
+
),
|
|
3310
|
+
copiedKey === key && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.copiedHint, children: "Copied" })
|
|
3311
|
+
]
|
|
3312
|
+
},
|
|
3313
|
+
key
|
|
3314
|
+
);
|
|
3315
|
+
}) })
|
|
3316
|
+
}
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
function formatValue2(value) {
|
|
3320
|
+
return value === null ? "NULL" : String(value);
|
|
3321
|
+
}
|
|
3322
|
+
function MobileDataTable({ columns, rows, columnWidths: columnWidthsProp }) {
|
|
3323
|
+
const { width: windowWidth } = reactNative.useWindowDimensions();
|
|
3324
|
+
const [copiedKey, setCopiedKey] = react.useState(null);
|
|
3325
|
+
const columnWidths = react.useMemo(
|
|
3326
|
+
() => columnWidthsProp ?? computeColumnWidths(columns, rows),
|
|
3327
|
+
[columnWidthsProp, columns, rows]
|
|
3328
|
+
);
|
|
3329
|
+
const tableWidth = react.useMemo(() => totalTableWidth(columnWidths), [columnWidths]);
|
|
3330
|
+
const showScrollHint = tableWidth > windowWidth - 32;
|
|
3331
|
+
const copyCell = async (value, key) => {
|
|
3332
|
+
await copyToClipboard();
|
|
3333
|
+
setCopiedKey(key);
|
|
3334
|
+
setTimeout(() => setCopiedKey(null), 1500);
|
|
3335
|
+
};
|
|
3336
|
+
if (columns.length === 0) {
|
|
3337
|
+
return null;
|
|
3338
|
+
}
|
|
3339
|
+
const headerRow = /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: tableStyles.headerRow, children: columns.map((column, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3340
|
+
reactNative.View,
|
|
3341
|
+
{
|
|
3342
|
+
style: [tableStyles.cell, tableStyles.headerCell, { width: columnWidths[index] }],
|
|
3343
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.headerText, children: column })
|
|
3344
|
+
},
|
|
3345
|
+
column
|
|
3346
|
+
)) });
|
|
3347
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: tableStyles.tableContainer, children: [
|
|
3348
|
+
showScrollHint && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.scrollHint, children: "Swipe horizontally for more columns" }),
|
|
3349
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3350
|
+
reactNative.ScrollView,
|
|
3351
|
+
{
|
|
3352
|
+
horizontal: true,
|
|
3353
|
+
bounces: false,
|
|
3354
|
+
nestedScrollEnabled: true,
|
|
3355
|
+
showsHorizontalScrollIndicator: true,
|
|
3356
|
+
style: tableStyles.horizontalScroll,
|
|
3357
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { width: tableWidth, flex: 1 }, children: [
|
|
3358
|
+
headerRow,
|
|
3359
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3360
|
+
reactNative.FlatList,
|
|
3361
|
+
{
|
|
3362
|
+
data: rows,
|
|
3363
|
+
keyExtractor: (_, index) => String(index),
|
|
3364
|
+
nestedScrollEnabled: true,
|
|
3365
|
+
style: tableStyles.bodyList,
|
|
3366
|
+
renderItem: ({ item: row, index: rowIndex }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [tableStyles.dataRow, rowIndex % 2 === 1 && tableStyles.dataRowAlt], children: row.map((cell, cellIndex) => {
|
|
3367
|
+
const column = columns[cellIndex];
|
|
3368
|
+
const display = formatValue2(cell);
|
|
3369
|
+
const key = `${rowIndex}-${cellIndex}`;
|
|
3370
|
+
const isNull = cell === null;
|
|
3371
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3372
|
+
reactNative.Pressable,
|
|
3373
|
+
{
|
|
3374
|
+
onPress: () => void copyCell(display, key),
|
|
3375
|
+
style: [tableStyles.cell, { width: columnWidths[cellIndex] }],
|
|
3376
|
+
children: [
|
|
3377
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3378
|
+
reactNative.Text,
|
|
3379
|
+
{
|
|
3380
|
+
numberOfLines: 2,
|
|
3381
|
+
selectable: true,
|
|
3382
|
+
style: [
|
|
3383
|
+
tableStyles.cellText,
|
|
3384
|
+
isMonospaceColumn(column) && tableStyles.cellTextMono,
|
|
3385
|
+
isNull && tableStyles.cellNull
|
|
3386
|
+
],
|
|
3387
|
+
children: display
|
|
3388
|
+
}
|
|
3389
|
+
),
|
|
3390
|
+
copiedKey === key && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.copiedHint, children: "Copied" })
|
|
3391
|
+
]
|
|
3392
|
+
},
|
|
3393
|
+
key
|
|
3394
|
+
);
|
|
3395
|
+
}) })
|
|
3396
|
+
}
|
|
3397
|
+
)
|
|
3398
|
+
] })
|
|
3399
|
+
}
|
|
3400
|
+
)
|
|
3401
|
+
] });
|
|
3402
|
+
}
|
|
3403
|
+
function MobileDataView({
|
|
3404
|
+
columns,
|
|
3405
|
+
rows,
|
|
3406
|
+
metaSuffix,
|
|
3407
|
+
defaultMode = "table",
|
|
3408
|
+
dataKey
|
|
3409
|
+
}) {
|
|
3410
|
+
const [mode, setMode] = react.useState(defaultMode);
|
|
3411
|
+
const columnWidths = react.useMemo(() => computeColumnWidths(columns, rows), [columns, rows]);
|
|
3412
|
+
const tableWidth = react.useMemo(() => totalTableWidth(columnWidths), [columnWidths]);
|
|
3413
|
+
const columnsKey = columns.join("|");
|
|
3414
|
+
react.useEffect(() => {
|
|
3415
|
+
setMode(defaultMode);
|
|
3416
|
+
}, [columnsKey, dataKey, defaultMode, rows.length]);
|
|
3417
|
+
if (columns.length === 0) {
|
|
3418
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "No rows to display." });
|
|
3419
|
+
}
|
|
3420
|
+
const meta = `${rows.length} row${rows.length === 1 ? "" : "s"} \xB7 ${columns.length} column${columns.length === 1 ? "" : "s"}${metaSuffix ? ` \xB7 ${metaSuffix}` : ""}`;
|
|
3421
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: tableStyles.viewRoot, children: [
|
|
3422
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: tableStyles.viewToolbar, children: [
|
|
3423
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.viewMeta, children: meta }),
|
|
3424
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: tableStyles.viewToggle, children: [
|
|
3425
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3426
|
+
reactNative.Pressable,
|
|
3427
|
+
{
|
|
3428
|
+
onPress: () => setMode("cards"),
|
|
3429
|
+
style: [tableStyles.viewToggleButton, mode === "cards" && tableStyles.viewToggleButtonActive],
|
|
3430
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3431
|
+
reactNative.Text,
|
|
3432
|
+
{
|
|
3433
|
+
style: [tableStyles.viewToggleLabel, mode === "cards" && tableStyles.viewToggleLabelActive],
|
|
3434
|
+
children: "Cards"
|
|
3435
|
+
}
|
|
3436
|
+
)
|
|
3437
|
+
}
|
|
3438
|
+
),
|
|
3439
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3440
|
+
reactNative.Pressable,
|
|
3441
|
+
{
|
|
3442
|
+
onPress: () => setMode("table"),
|
|
3443
|
+
style: [tableStyles.viewToggleButton, mode === "table" && tableStyles.viewToggleButtonActive],
|
|
3444
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3445
|
+
reactNative.Text,
|
|
3446
|
+
{
|
|
3447
|
+
style: [tableStyles.viewToggleLabel, mode === "table" && tableStyles.viewToggleLabelActive],
|
|
3448
|
+
children: "Table"
|
|
3449
|
+
}
|
|
3450
|
+
)
|
|
3451
|
+
}
|
|
3452
|
+
)
|
|
3453
|
+
] })
|
|
3454
|
+
] }),
|
|
3455
|
+
mode === "cards" ? /* @__PURE__ */ jsxRuntime.jsx(MobileDataCardList, { columns, rows }) : /* @__PURE__ */ jsxRuntime.jsx(MobileDataTable, { columnWidths, columns, rows }),
|
|
3456
|
+
mode === "table" && tableWidth > 320 && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.footerHint, children: "Tap a cell to copy its value" }),
|
|
3457
|
+
mode === "cards" && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: tableStyles.footerHint, children: "Tap a field to copy its value" })
|
|
3458
|
+
] });
|
|
3459
|
+
}
|
|
3460
|
+
function ExplorerSqlTab({ inspector }) {
|
|
3461
|
+
const [sql, setSql] = react.useState(DEFAULT_MOBILE_SQL);
|
|
3462
|
+
const [result, setResult] = react.useState(null);
|
|
3463
|
+
const [error, setError] = react.useState(null);
|
|
3464
|
+
const [running, setRunning] = react.useState(false);
|
|
3465
|
+
const [panel, setPanel] = react.useState("results");
|
|
3466
|
+
const [resultKey, setResultKey] = react.useState(0);
|
|
3467
|
+
const runQuery = async () => {
|
|
3468
|
+
setRunning(true);
|
|
3469
|
+
setError(null);
|
|
3470
|
+
try {
|
|
3471
|
+
const queryResult = await inspector.executeQuery(sql);
|
|
3472
|
+
setResult(queryResult);
|
|
3473
|
+
setResultKey((current) => current + 1);
|
|
3474
|
+
setPanel("results");
|
|
3475
|
+
} catch (runError) {
|
|
3476
|
+
const message = runError instanceof Error ? runError.message : "Query failed";
|
|
3477
|
+
setError(message);
|
|
3478
|
+
setResult(null);
|
|
3479
|
+
setPanel("output");
|
|
3480
|
+
} finally {
|
|
3481
|
+
setRunning(false);
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
const renderResultsContent = () => {
|
|
3485
|
+
if (panel === "output") {
|
|
3486
|
+
if (error) {
|
|
3487
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.errorText, children: error });
|
|
3488
|
+
}
|
|
3489
|
+
if (result) {
|
|
3490
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: explorerStyles.infoValue, children: [
|
|
3491
|
+
"Query executed successfully. ",
|
|
3492
|
+
result.rowCount,
|
|
3493
|
+
" row",
|
|
3494
|
+
result.rowCount === 1 ? "" : "s",
|
|
3495
|
+
" in ",
|
|
3496
|
+
result.durationMs.toFixed(1),
|
|
3497
|
+
" ms."
|
|
3498
|
+
] });
|
|
3499
|
+
}
|
|
3500
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "Messages and errors appear here." });
|
|
3501
|
+
}
|
|
3502
|
+
if (!result) {
|
|
3503
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: explorerStyles.sqlResultsEmpty, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [explorerStyles.placeholder, { marginTop: 0 }], children: "Run a query to see results." }) });
|
|
3504
|
+
}
|
|
3505
|
+
if (result.columns.length === 0) {
|
|
3506
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: explorerStyles.sqlResultsEmpty, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [explorerStyles.placeholder, { marginTop: 0 }], children: [
|
|
3507
|
+
"Query completed with no result set (",
|
|
3508
|
+
result.durationMs.toFixed(1),
|
|
3509
|
+
" ms)."
|
|
3510
|
+
] }) });
|
|
3511
|
+
}
|
|
3512
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3513
|
+
MobileDataView,
|
|
3514
|
+
{
|
|
3515
|
+
columns: result.columns,
|
|
3516
|
+
dataKey: String(resultKey),
|
|
3517
|
+
defaultMode: "table",
|
|
3518
|
+
metaSuffix: `${result.durationMs.toFixed(1)} ms`,
|
|
3519
|
+
rows: result.rows
|
|
3520
|
+
}
|
|
3521
|
+
);
|
|
3522
|
+
};
|
|
3523
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.sqlTabRoot, children: [
|
|
3524
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.sqlQueryPane, children: [
|
|
3525
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.sectionTitle, children: "SQL (read-only)" }),
|
|
3526
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3527
|
+
reactNative.TextInput,
|
|
3528
|
+
{
|
|
3529
|
+
autoCapitalize: "none",
|
|
3530
|
+
autoCorrect: false,
|
|
3531
|
+
multiline: true,
|
|
3532
|
+
onChangeText: setSql,
|
|
3533
|
+
scrollEnabled: true,
|
|
3534
|
+
style: explorerStyles.sqlInputCompact,
|
|
3535
|
+
value: sql
|
|
3536
|
+
}
|
|
3537
|
+
),
|
|
3538
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3539
|
+
reactNative.Pressable,
|
|
3540
|
+
{
|
|
3541
|
+
disabled: running,
|
|
3542
|
+
onPress: () => void runQuery(),
|
|
3543
|
+
style: [explorerStyles.primaryButton, running && explorerStyles.primaryButtonDisabled],
|
|
3544
|
+
children: running ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: "#f8fafc" }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.primaryButtonLabel, children: "Run query" })
|
|
3545
|
+
}
|
|
3546
|
+
),
|
|
3547
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [explorerStyles.hintText, { marginTop: 0 }], children: "Allowed: SELECT, PRAGMA, EXPLAIN, WITH. Writes are blocked in DevTools." })
|
|
3548
|
+
] }),
|
|
3549
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [explorerStyles.tabRow, { flexShrink: 0 }], children: [
|
|
3550
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3551
|
+
reactNative.Pressable,
|
|
3552
|
+
{
|
|
3553
|
+
onPress: () => setPanel("results"),
|
|
3554
|
+
style: [explorerStyles.tab, panel === "results" && explorerStyles.tabActive],
|
|
3555
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3556
|
+
reactNative.Text,
|
|
3557
|
+
{
|
|
3558
|
+
style: [explorerStyles.tabLabel, panel === "results" && explorerStyles.tabLabelActive],
|
|
3559
|
+
children: [
|
|
3560
|
+
"Results",
|
|
3561
|
+
result ? ` (${result.rowCount})` : ""
|
|
3562
|
+
]
|
|
3563
|
+
}
|
|
3564
|
+
)
|
|
3565
|
+
}
|
|
3566
|
+
),
|
|
3567
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3568
|
+
reactNative.Pressable,
|
|
3569
|
+
{
|
|
3570
|
+
onPress: () => setPanel("output"),
|
|
3571
|
+
style: [explorerStyles.tab, panel === "output" && explorerStyles.tabActive],
|
|
3572
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3573
|
+
reactNative.Text,
|
|
3574
|
+
{
|
|
3575
|
+
style: [explorerStyles.tabLabel, panel === "output" && explorerStyles.tabLabelActive],
|
|
3576
|
+
children: "Output"
|
|
3577
|
+
}
|
|
3578
|
+
)
|
|
3579
|
+
}
|
|
3580
|
+
)
|
|
3581
|
+
] }),
|
|
3582
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: explorerStyles.sqlResultsPane, children: renderResultsContent() })
|
|
3583
|
+
] });
|
|
3584
|
+
}
|
|
3585
|
+
function ExplorerTableDetail({ inspector, tableName, onBack }) {
|
|
3586
|
+
const [view, setView] = react.useState("data");
|
|
3587
|
+
const [columns, setColumns] = react.useState([]);
|
|
3588
|
+
const [pageResult, setPageResult] = react.useState(null);
|
|
3589
|
+
const [search, setSearch] = react.useState("");
|
|
3590
|
+
const [page, setPage] = react.useState(1);
|
|
3591
|
+
const [loading, setLoading] = react.useState(true);
|
|
3592
|
+
const [error, setError] = react.useState(null);
|
|
3593
|
+
const loadData = react.useCallback(async () => {
|
|
3594
|
+
setLoading(true);
|
|
3595
|
+
setError(null);
|
|
3596
|
+
try {
|
|
3597
|
+
const nextColumns = await inspector.getTableColumns(tableName);
|
|
3598
|
+
setColumns(nextColumns);
|
|
3599
|
+
const result = await inspector.fetchTablePage({
|
|
3600
|
+
table: tableName,
|
|
3601
|
+
page,
|
|
3602
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
3603
|
+
search
|
|
3604
|
+
});
|
|
3605
|
+
setPageResult(result);
|
|
3606
|
+
} catch (loadError) {
|
|
3607
|
+
setError(loadError instanceof Error ? loadError.message : "Failed to load table");
|
|
3608
|
+
} finally {
|
|
3609
|
+
setLoading(false);
|
|
3610
|
+
}
|
|
3611
|
+
}, [inspector, page, search, tableName]);
|
|
3612
|
+
react.useEffect(() => {
|
|
3613
|
+
void loadData();
|
|
3614
|
+
}, [loadData]);
|
|
3615
|
+
const totalPages = pageResult ? Math.max(1, Math.ceil(pageResult.totalCount / pageResult.pageSize)) : 1;
|
|
3616
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.fullScreen, children: [
|
|
3617
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.header, children: [
|
|
3618
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onBack, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.headerAction, children: "\u2190 Back" }) }),
|
|
3619
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.headerTitle, children: tableName }),
|
|
3620
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { width: 48 } })
|
|
3621
|
+
] }),
|
|
3622
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [explorerStyles.subTabRow, { paddingHorizontal: 16, paddingTop: 12 }], children: [
|
|
3623
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3624
|
+
reactNative.Pressable,
|
|
3625
|
+
{
|
|
3626
|
+
onPress: () => setView("data"),
|
|
3627
|
+
style: [explorerStyles.subTab, view === "data" && explorerStyles.subTabActive],
|
|
3628
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [explorerStyles.subTabLabel, view === "data" && explorerStyles.subTabLabelActive], children: "Data" })
|
|
3629
|
+
}
|
|
3630
|
+
),
|
|
3631
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3632
|
+
reactNative.Pressable,
|
|
3633
|
+
{
|
|
3634
|
+
onPress: () => setView("schema"),
|
|
3635
|
+
style: [explorerStyles.subTab, view === "schema" && explorerStyles.subTabActive],
|
|
3636
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3637
|
+
reactNative.Text,
|
|
3638
|
+
{
|
|
3639
|
+
style: [explorerStyles.subTabLabel, view === "schema" && explorerStyles.subTabLabelActive],
|
|
3640
|
+
children: "Schema"
|
|
3641
|
+
}
|
|
3642
|
+
)
|
|
3643
|
+
}
|
|
3644
|
+
)
|
|
3645
|
+
] }),
|
|
3646
|
+
view === "data" ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: tableStyles.detailBody, children: [
|
|
3647
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3648
|
+
reactNative.TextInput,
|
|
3649
|
+
{
|
|
3650
|
+
autoCapitalize: "none",
|
|
3651
|
+
autoCorrect: false,
|
|
3652
|
+
onChangeText: (value) => {
|
|
3653
|
+
setSearch(value);
|
|
3654
|
+
setPage(1);
|
|
3655
|
+
},
|
|
3656
|
+
placeholder: "Search rows\u2026",
|
|
3657
|
+
style: explorerStyles.searchInput,
|
|
3658
|
+
value: search
|
|
3659
|
+
}
|
|
3660
|
+
),
|
|
3661
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { style: { marginTop: 24 } }) : error ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.errorText, children: error }) : pageResult ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3662
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: tableStyles.detailDataPane, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3663
|
+
MobileDataView,
|
|
3664
|
+
{
|
|
3665
|
+
columns: pageResult.columns,
|
|
3666
|
+
dataKey: `${tableName}-${page}-${search}`,
|
|
3667
|
+
defaultMode: "table",
|
|
3668
|
+
metaSuffix: `page ${page} of ${totalPages}`,
|
|
3669
|
+
rows: pageResult.rows
|
|
3670
|
+
}
|
|
3671
|
+
) }),
|
|
3672
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.paginationRow, children: [
|
|
3673
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { disabled: page <= 1, onPress: () => setPage((current) => current - 1), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.headerAction, children: "Previous" }) }),
|
|
3674
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: explorerStyles.paginationLabel, children: [
|
|
3675
|
+
"Page ",
|
|
3676
|
+
page,
|
|
3677
|
+
" of ",
|
|
3678
|
+
totalPages,
|
|
3679
|
+
" \xB7 ",
|
|
3680
|
+
pageResult.totalCount.toLocaleString(),
|
|
3681
|
+
" rows"
|
|
3682
|
+
] }),
|
|
3683
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3684
|
+
reactNative.Pressable,
|
|
3685
|
+
{
|
|
3686
|
+
disabled: page >= totalPages,
|
|
3687
|
+
onPress: () => setPage((current) => current + 1),
|
|
3688
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.headerAction, children: "Next" })
|
|
3689
|
+
}
|
|
3690
|
+
)
|
|
3691
|
+
] })
|
|
3692
|
+
] }) : null
|
|
3693
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { contentContainerStyle: explorerStyles.content, children: columns.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "No schema information." }) : columns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: explorerStyles.listItem, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
|
|
3694
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.listItemTitle, children: column.name }),
|
|
3695
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: explorerStyles.listItemMeta, children: [
|
|
3696
|
+
column.type,
|
|
3697
|
+
column.pk ? " \xB7 PK" : "",
|
|
3698
|
+
column.notNull ? " \xB7 NOT NULL" : "",
|
|
3699
|
+
column.defaultValue ? ` \xB7 DEFAULT ${column.defaultValue}` : ""
|
|
3700
|
+
] })
|
|
3701
|
+
] }) }, column.name)) })
|
|
3702
|
+
] });
|
|
3703
|
+
}
|
|
3704
|
+
function ExplorerTablesTab({
|
|
3705
|
+
tables,
|
|
3706
|
+
loading,
|
|
3707
|
+
error,
|
|
3708
|
+
onSelectTable
|
|
3709
|
+
}) {
|
|
3710
|
+
const [search, setSearch] = react.useState("");
|
|
3711
|
+
const filtered = react.useMemo(() => {
|
|
3712
|
+
const term = search.trim().toLowerCase();
|
|
3713
|
+
if (!term) {
|
|
3714
|
+
return tables;
|
|
3715
|
+
}
|
|
3716
|
+
return tables.filter((table) => table.name.toLowerCase().includes(term));
|
|
3717
|
+
}, [search, tables]);
|
|
3718
|
+
if (loading) {
|
|
3719
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "Loading tables\u2026" });
|
|
3720
|
+
}
|
|
3721
|
+
if (error) {
|
|
3722
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.errorText, children: error });
|
|
3723
|
+
}
|
|
3724
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, padding: 16 }, children: [
|
|
3725
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3726
|
+
reactNative.TextInput,
|
|
3727
|
+
{
|
|
3728
|
+
autoCapitalize: "none",
|
|
3729
|
+
autoCorrect: false,
|
|
3730
|
+
onChangeText: setSearch,
|
|
3731
|
+
placeholder: "Filter tables\u2026",
|
|
3732
|
+
style: explorerStyles.searchInput,
|
|
3733
|
+
value: search
|
|
3734
|
+
}
|
|
3735
|
+
),
|
|
3736
|
+
filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.placeholder, children: "No tables match your search." }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3737
|
+
reactNative.FlatList,
|
|
3738
|
+
{
|
|
3739
|
+
data: filtered,
|
|
3740
|
+
keyExtractor: (item) => item.name,
|
|
3741
|
+
style: { flex: 1 },
|
|
3742
|
+
renderItem: ({ item }) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress: () => onSelectTable(item.name), style: explorerStyles.listItem, children: [
|
|
3743
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
|
|
3744
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.listItemTitle, children: item.name }),
|
|
3745
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: explorerStyles.listItemMeta, children: [
|
|
3746
|
+
item.rowCount.toLocaleString(),
|
|
3747
|
+
" row",
|
|
3748
|
+
item.rowCount === 1 ? "" : "s"
|
|
3749
|
+
] })
|
|
3750
|
+
] }),
|
|
3751
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.listItemMeta, children: "\u203A" })
|
|
3752
|
+
] })
|
|
3753
|
+
}
|
|
3754
|
+
)
|
|
3755
|
+
] });
|
|
3756
|
+
}
|
|
3757
|
+
function MobileDatabaseExplorer() {
|
|
3758
|
+
const { explorerVisible, closeExplorer, mobileInspector } = useDevTools();
|
|
3759
|
+
const [tab, setTab] = react.useState("overview");
|
|
3760
|
+
const [screen, setScreen] = react.useState({ kind: "tabs" });
|
|
3761
|
+
const [info, setInfo] = react.useState(null);
|
|
3762
|
+
const [tables, setTables] = react.useState([]);
|
|
3763
|
+
const [loading, setLoading] = react.useState(false);
|
|
3764
|
+
const [error, setError] = react.useState(null);
|
|
3765
|
+
const loadOverview = react.useCallback(async () => {
|
|
3766
|
+
if (!mobileInspector) {
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
setLoading(true);
|
|
3770
|
+
setError(null);
|
|
3771
|
+
try {
|
|
3772
|
+
const [nextInfo, nextTables] = await Promise.all([
|
|
3773
|
+
mobileInspector.getDatabaseInfo(),
|
|
3774
|
+
mobileInspector.listTables()
|
|
3775
|
+
]);
|
|
3776
|
+
setInfo(nextInfo);
|
|
3777
|
+
setTables(nextTables);
|
|
3778
|
+
} catch (loadError) {
|
|
3779
|
+
setError(loadError instanceof Error ? loadError.message : "Failed to load database");
|
|
3780
|
+
} finally {
|
|
3781
|
+
setLoading(false);
|
|
3782
|
+
}
|
|
3783
|
+
}, [mobileInspector]);
|
|
3784
|
+
react.useEffect(() => {
|
|
3785
|
+
if (!explorerVisible) {
|
|
3786
|
+
setScreen({ kind: "tabs" });
|
|
3787
|
+
setTab("overview");
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
void loadOverview();
|
|
3791
|
+
}, [explorerVisible, loadOverview]);
|
|
3792
|
+
if (!mobileInspector) {
|
|
3793
|
+
return null;
|
|
3794
|
+
}
|
|
3795
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { animationType: "slide", onRequestClose: closeExplorer, visible: explorerVisible, children: screen.kind === "table" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3796
|
+
ExplorerTableDetail,
|
|
3797
|
+
{
|
|
3798
|
+
inspector: mobileInspector,
|
|
3799
|
+
onBack: () => setScreen({ kind: "tabs" }),
|
|
3800
|
+
tableName: screen.tableName
|
|
3801
|
+
}
|
|
3802
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.fullScreen, children: [
|
|
3803
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: explorerStyles.header, children: [
|
|
3804
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.headerTitle, children: "Database Explorer" }),
|
|
3805
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { accessibilityLabel: "Close database explorer", onPress: closeExplorer, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: explorerStyles.headerAction, children: "Close" }) })
|
|
3806
|
+
] }),
|
|
3807
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: explorerStyles.tabRow, children: ["overview", "tables", "sql"].map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3808
|
+
reactNative.Pressable,
|
|
3809
|
+
{
|
|
3810
|
+
onPress: () => setTab(item),
|
|
3811
|
+
style: [explorerStyles.tab, tab === item && explorerStyles.tabActive],
|
|
3812
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3813
|
+
reactNative.Text,
|
|
3814
|
+
{
|
|
3815
|
+
style: [explorerStyles.tabLabel, tab === item && explorerStyles.tabLabelActive],
|
|
3816
|
+
children: item === "overview" ? "Overview" : item === "tables" ? "Tables" : "SQL"
|
|
3817
|
+
}
|
|
3818
|
+
)
|
|
3819
|
+
},
|
|
3820
|
+
item
|
|
3821
|
+
)) }),
|
|
3822
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
|
|
3823
|
+
tab === "overview" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3824
|
+
ExplorerOverviewTab,
|
|
3825
|
+
{
|
|
3826
|
+
error,
|
|
3827
|
+
info,
|
|
3828
|
+
loading,
|
|
3829
|
+
onOpenSql: () => setTab("sql"),
|
|
3830
|
+
onOpenTables: () => setTab("tables")
|
|
3831
|
+
}
|
|
3832
|
+
),
|
|
3833
|
+
tab === "tables" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3834
|
+
ExplorerTablesTab,
|
|
3835
|
+
{
|
|
3836
|
+
error,
|
|
3837
|
+
loading,
|
|
3838
|
+
onSelectTable: (tableName) => setScreen({ kind: "table", tableName }),
|
|
3839
|
+
tables
|
|
3840
|
+
}
|
|
3841
|
+
),
|
|
3842
|
+
tab === "sql" && /* @__PURE__ */ jsxRuntime.jsx(ExplorerSqlTab, { inspector: mobileInspector })
|
|
3843
|
+
] })
|
|
3844
|
+
] }) });
|
|
3845
|
+
}
|
|
3846
|
+
function DatabaseDevTools({
|
|
3847
|
+
database,
|
|
3848
|
+
type,
|
|
3849
|
+
adapter,
|
|
3850
|
+
serverUrl,
|
|
3851
|
+
enabled,
|
|
3852
|
+
position = "bottom-right",
|
|
3853
|
+
draggable = true,
|
|
3854
|
+
snapToEdges = true,
|
|
3855
|
+
floatingPosition,
|
|
3856
|
+
onFloatingPositionChange,
|
|
3857
|
+
style,
|
|
3858
|
+
onConnectionStateChange
|
|
3859
|
+
}) {
|
|
3860
|
+
if (!isDevToolsEnabled(enabled)) {
|
|
3861
|
+
return null;
|
|
3862
|
+
}
|
|
3863
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3864
|
+
DevToolsProvider,
|
|
3865
|
+
{
|
|
3866
|
+
adapter,
|
|
3867
|
+
database,
|
|
3868
|
+
onConnectionStateChange,
|
|
3869
|
+
serverUrl,
|
|
3870
|
+
type,
|
|
3871
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { pointerEvents: "box-none", style: reactNative.StyleSheet.absoluteFill, children: [
|
|
3872
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3873
|
+
FloatingDevToolsButton,
|
|
3874
|
+
{
|
|
3875
|
+
draggable,
|
|
3876
|
+
floatingPosition,
|
|
3877
|
+
iconStyle: style,
|
|
3878
|
+
onFloatingPositionChange,
|
|
3879
|
+
position,
|
|
3880
|
+
snapToEdges
|
|
3881
|
+
}
|
|
3882
|
+
),
|
|
3883
|
+
/* @__PURE__ */ jsxRuntime.jsx(DevToolsLauncherModal, {}),
|
|
3884
|
+
/* @__PURE__ */ jsxRuntime.jsx(DevToolsSettingsModal, {}),
|
|
3885
|
+
/* @__PURE__ */ jsxRuntime.jsx(MobileDatabaseExplorer, {})
|
|
3886
|
+
] })
|
|
3887
|
+
}
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
exports.AdapterResolutionError = AdapterResolutionError;
|
|
3892
|
+
exports.DATABASE_DEVTOOLS_KIND = DATABASE_DEVTOOLS_KIND;
|
|
3893
|
+
exports.DEFAULT_DEVTOOLS_HOST = DEFAULT_DEVTOOLS_HOST;
|
|
3894
|
+
exports.DEFAULT_DEVTOOLS_PORT = DEFAULT_DEVTOOLS_PORT;
|
|
3895
|
+
exports.DEVICE_SNAPSHOT_API_PATH = DEVICE_SNAPSHOT_API_PATH;
|
|
3896
|
+
exports.DEVTOOLS_WS_PATH = DEVTOOLS_WS_PATH;
|
|
3897
|
+
exports.DatabaseDevTools = DatabaseDevTools;
|
|
3898
|
+
exports.DevToolsRole = DevToolsRole;
|
|
3899
|
+
exports.HEARTBEAT_INTERVAL_MS = HEARTBEAT_INTERVAL_MS;
|
|
3900
|
+
exports.HEARTBEAT_TIMEOUT_MS = HEARTBEAT_TIMEOUT_MS;
|
|
3901
|
+
exports.MAX_SNAPSHOT_BYTES = MAX_SNAPSHOT_BYTES;
|
|
3902
|
+
exports.MessageType = MessageType;
|
|
3903
|
+
exports.REFRESH_TIMEOUT_MS = REFRESH_TIMEOUT_MS;
|
|
3904
|
+
exports.SNAPSHOT_API_PATH = SNAPSHOT_API_PATH;
|
|
3905
|
+
exports.SNAPSHOT_KIND_HEADER = SNAPSHOT_KIND_HEADER;
|
|
3906
|
+
exports.SNAPSHOT_MIME_HEADER = SNAPSHOT_MIME_HEADER;
|
|
3907
|
+
exports.SNAPSHOT_NAME_HEADER = SNAPSHOT_NAME_HEADER;
|
|
3908
|
+
exports.SQLITE_SNAPSHOT_MIME_TYPE = SQLITE_SNAPSHOT_MIME_TYPE;
|
|
3909
|
+
exports.SYNC_TIMEOUT_MS = SYNC_TIMEOUT_MS;
|
|
3910
|
+
exports.buildDevToolsHttpUrl = buildDevToolsHttpUrl;
|
|
3911
|
+
exports.buildDevToolsWsUrl = buildDevToolsWsUrl;
|
|
3912
|
+
exports.buildDeviceSnapshotUrl = buildDeviceSnapshotUrl;
|
|
3913
|
+
exports.buildSnapshotUrl = buildSnapshotUrl;
|
|
3914
|
+
exports.createDevToolsClient = createDevToolsClient;
|
|
3915
|
+
exports.createExpoSqliteAdapter = createExpoSqliteAdapter;
|
|
3916
|
+
exports.createExpoSqliteInspector = createExpoSqliteInspector;
|
|
3917
|
+
exports.createInspectorForSnapshot = createInspectorForSnapshot;
|
|
3918
|
+
exports.createMessage = createMessage;
|
|
3919
|
+
exports.detectExpoSqlite = detectExpoSqlite;
|
|
3920
|
+
exports.detectExpoSqliteInspectable = detectExpoSqliteInspectable;
|
|
3921
|
+
exports.fetchSnapshot = fetchSnapshot;
|
|
3922
|
+
exports.getAdapterRegistry = getAdapterRegistry;
|
|
3923
|
+
exports.getInspectorRegistry = getInspectorRegistry;
|
|
3924
|
+
exports.handleDeviceSnapshotUpload = handleDeviceSnapshotUpload;
|
|
3925
|
+
exports.isBroadcastMessage = isBroadcastMessage;
|
|
3926
|
+
exports.isClientMessage = isClientMessage;
|
|
3927
|
+
exports.isDeviceStatusMessage = isDeviceStatusMessage;
|
|
3928
|
+
exports.isEditableDatabaseAdapter = isEditableDatabaseAdapter;
|
|
3929
|
+
exports.isPingMessage = isPingMessage;
|
|
3930
|
+
exports.isPongMessage = isPongMessage;
|
|
3931
|
+
exports.isRefreshErrorMessage = isRefreshErrorMessage;
|
|
3932
|
+
exports.isRefreshRequestMessage = isRefreshRequestMessage;
|
|
3933
|
+
exports.isRefreshStatusMessage = isRefreshStatusMessage;
|
|
3934
|
+
exports.isRegisterMessage = isRegisterMessage;
|
|
3935
|
+
exports.isServerMessage = isServerMessage;
|
|
3936
|
+
exports.isSnapshotReadyMessage = isSnapshotReadyMessage;
|
|
3937
|
+
exports.isSnapshotUploadRequestedMessage = isSnapshotUploadRequestedMessage;
|
|
3938
|
+
exports.isWritableDatabaseAdapter = isWritableDatabaseAdapter;
|
|
3939
|
+
exports.readDatabaseKindMarker = readDatabaseKindMarker;
|
|
3940
|
+
exports.registerBuiltInAdapters = registerBuiltInAdapters;
|
|
3941
|
+
exports.registerSqliteAdapter = registerSqliteAdapter;
|
|
3942
|
+
exports.resolveAdapter = resolveAdapter;
|
|
3943
|
+
exports.sqliteAdapterDefinition = sqliteAdapterDefinition;
|
|
3944
|
+
exports.wsUrlToHttpUrl = wsUrlToHttpUrl;
|
|
3945
|
+
//# sourceMappingURL=native.cjs.map
|
|
3946
|
+
//# sourceMappingURL=native.cjs.map
|