@adventurelabs/scout-core 1.0.83 → 1.0.85
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/helpers/cache.d.ts +7 -2
- package/dist/helpers/cache.js +116 -123
- package/dist/helpers/heartbeats.d.ts +9 -0
- package/dist/helpers/heartbeats.js +90 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/hooks/useScoutRefresh.d.ts +3 -1
- package/dist/hooks/useScoutRefresh.js +71 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/ScoutRefreshProvider.d.ts +27 -0
- package/dist/types/db.d.ts +1 -1
- package/dist/types/herd_module.d.ts +2 -4
- package/dist/types/herd_module.js +6 -23
- package/dist/types/supabase.d.ts +29 -0
- package/package.json +1 -1
package/dist/helpers/cache.d.ts
CHANGED
|
@@ -30,11 +30,16 @@ export interface TimingStats {
|
|
|
30
30
|
dataProcessing: number;
|
|
31
31
|
localStorage: number;
|
|
32
32
|
}
|
|
33
|
+
export interface DatabaseHealth {
|
|
34
|
+
healthy: boolean;
|
|
35
|
+
issues: string[];
|
|
36
|
+
}
|
|
33
37
|
export declare class ScoutCache {
|
|
34
38
|
private db;
|
|
35
39
|
private initPromise;
|
|
36
40
|
private stats;
|
|
37
41
|
private init;
|
|
42
|
+
private validateDatabaseSchema;
|
|
38
43
|
setHerdModules(herdModules: IHerdModule[], ttlMs?: number, etag?: string): Promise<void>;
|
|
39
44
|
getHerdModules(): Promise<CacheResult<IHerdModule[]>>;
|
|
40
45
|
clearHerdModules(): Promise<void>;
|
|
@@ -47,8 +52,8 @@ export declare class ScoutCache {
|
|
|
47
52
|
reason: string;
|
|
48
53
|
}>;
|
|
49
54
|
preloadCache(loadFunction: () => Promise<IHerdModule[]>, ttlMs?: number): Promise<void>;
|
|
50
|
-
getProvidersForHerd(herdId: string): Promise<CacheResult<any[]>>;
|
|
51
|
-
setProvidersForHerd(herdId: string, providers: any[], ttlMs?: number): Promise<void>;
|
|
52
55
|
getDefaultTtl(): number;
|
|
56
|
+
resetDatabase(): Promise<void>;
|
|
57
|
+
checkDatabaseHealth(): Promise<DatabaseHealth>;
|
|
53
58
|
}
|
|
54
59
|
export declare const scoutCache: ScoutCache;
|
package/dist/helpers/cache.js
CHANGED
|
@@ -2,7 +2,6 @@ const DB_NAME = "ScoutCache";
|
|
|
2
2
|
const DB_VERSION = 1;
|
|
3
3
|
const HERD_MODULES_STORE = "herd_modules";
|
|
4
4
|
const CACHE_METADATA_STORE = "cache_metadata";
|
|
5
|
-
const PROVIDERS_STORE = "providers";
|
|
6
5
|
// Default TTL: 24 hours (1 day)
|
|
7
6
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
7
|
export class ScoutCache {
|
|
@@ -23,58 +22,84 @@ export class ScoutCache {
|
|
|
23
22
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
24
23
|
request.onerror = () => {
|
|
25
24
|
console.error("[ScoutCache] Failed to open IndexedDB:", request.error);
|
|
25
|
+
this.db = null;
|
|
26
|
+
this.initPromise = null;
|
|
26
27
|
reject(request.error);
|
|
27
28
|
};
|
|
28
29
|
request.onsuccess = () => {
|
|
29
30
|
this.db = request.result;
|
|
31
|
+
// Validate that required object stores exist
|
|
32
|
+
if (!this.validateDatabaseSchema()) {
|
|
33
|
+
console.error("[ScoutCache] Database schema validation failed");
|
|
34
|
+
this.db.close();
|
|
35
|
+
this.db = null;
|
|
36
|
+
this.initPromise = null;
|
|
37
|
+
reject(new Error("Database schema validation failed"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
30
40
|
console.log("[ScoutCache] IndexedDB initialized successfully");
|
|
31
41
|
resolve();
|
|
32
42
|
};
|
|
33
43
|
request.onupgradeneeded = (event) => {
|
|
34
44
|
const db = event.target.result;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
try {
|
|
46
|
+
// Create herd modules store
|
|
47
|
+
if (!db.objectStoreNames.contains(HERD_MODULES_STORE)) {
|
|
48
|
+
const herdModulesStore = db.createObjectStore(HERD_MODULES_STORE, {
|
|
49
|
+
keyPath: "herdId",
|
|
50
|
+
});
|
|
51
|
+
herdModulesStore.createIndex("timestamp", "timestamp", {
|
|
52
|
+
unique: false,
|
|
53
|
+
});
|
|
54
|
+
console.log("[ScoutCache] Created herd_modules object store");
|
|
55
|
+
}
|
|
56
|
+
// Create cache metadata store
|
|
57
|
+
if (!db.objectStoreNames.contains(CACHE_METADATA_STORE)) {
|
|
58
|
+
const metadataStore = db.createObjectStore(CACHE_METADATA_STORE, {
|
|
59
|
+
keyPath: "key",
|
|
60
|
+
});
|
|
61
|
+
console.log("[ScoutCache] Created cache_metadata object store");
|
|
62
|
+
}
|
|
63
|
+
console.log("[ScoutCache] Database schema upgraded");
|
|
49
64
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
keyPath: "herdId",
|
|
54
|
-
});
|
|
55
|
-
providersStore.createIndex("timestamp", "timestamp", {
|
|
56
|
-
unique: false,
|
|
57
|
-
});
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error("[ScoutCache] Error during database upgrade:", error);
|
|
67
|
+
reject(error);
|
|
58
68
|
}
|
|
59
|
-
console.log("[ScoutCache] Database schema upgraded");
|
|
60
69
|
};
|
|
61
70
|
});
|
|
62
71
|
return this.initPromise;
|
|
63
72
|
}
|
|
73
|
+
validateDatabaseSchema() {
|
|
74
|
+
if (!this.db)
|
|
75
|
+
return false;
|
|
76
|
+
const hasHerdModulesStore = this.db.objectStoreNames.contains(HERD_MODULES_STORE);
|
|
77
|
+
const hasMetadataStore = this.db.objectStoreNames.contains(CACHE_METADATA_STORE);
|
|
78
|
+
if (!hasHerdModulesStore) {
|
|
79
|
+
console.error("[ScoutCache] Missing herd_modules object store");
|
|
80
|
+
}
|
|
81
|
+
if (!hasMetadataStore) {
|
|
82
|
+
console.error("[ScoutCache] Missing cache_metadata object store");
|
|
83
|
+
}
|
|
84
|
+
return hasHerdModulesStore && hasMetadataStore;
|
|
85
|
+
}
|
|
64
86
|
async setHerdModules(herdModules, ttlMs = DEFAULT_TTL_MS, etag) {
|
|
65
87
|
await this.init();
|
|
66
88
|
if (!this.db)
|
|
67
89
|
throw new Error("Database not initialized");
|
|
68
|
-
|
|
90
|
+
// Validate schema before creating transaction
|
|
91
|
+
if (!this.validateDatabaseSchema()) {
|
|
92
|
+
throw new Error("Database schema validation failed - required object stores not found");
|
|
93
|
+
}
|
|
94
|
+
const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readwrite");
|
|
69
95
|
return new Promise((resolve, reject) => {
|
|
70
96
|
transaction.onerror = () => reject(transaction.error);
|
|
71
97
|
transaction.oncomplete = () => resolve();
|
|
72
98
|
const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
|
|
73
99
|
const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
|
|
74
|
-
const providersStore = transaction.objectStore(PROVIDERS_STORE);
|
|
75
100
|
const timestamp = Date.now();
|
|
76
101
|
const version = "1.0.0";
|
|
77
|
-
// Store each herd module
|
|
102
|
+
// Store each herd module
|
|
78
103
|
herdModules.forEach((herdModule) => {
|
|
79
104
|
const cacheEntry = {
|
|
80
105
|
herdId: herdModule.herd.id.toString(),
|
|
@@ -82,15 +107,6 @@ export class ScoutCache {
|
|
|
82
107
|
timestamp,
|
|
83
108
|
};
|
|
84
109
|
herdModulesStore.put(cacheEntry);
|
|
85
|
-
// Store providers separately for easier querying
|
|
86
|
-
if (herdModule.providers && herdModule.providers.length > 0) {
|
|
87
|
-
const providersCacheEntry = {
|
|
88
|
-
herdId: herdModule.herd.id.toString(),
|
|
89
|
-
data: herdModule.providers,
|
|
90
|
-
timestamp,
|
|
91
|
-
};
|
|
92
|
-
providersStore.put(providersCacheEntry);
|
|
93
|
-
}
|
|
94
110
|
});
|
|
95
111
|
// Store cache metadata
|
|
96
112
|
const metadata = {
|
|
@@ -102,22 +118,16 @@ export class ScoutCache {
|
|
|
102
118
|
lastModified: timestamp,
|
|
103
119
|
};
|
|
104
120
|
metadataStore.put(metadata);
|
|
105
|
-
// Store providers metadata
|
|
106
|
-
const providersMetadata = {
|
|
107
|
-
key: "providers",
|
|
108
|
-
timestamp,
|
|
109
|
-
ttl: ttlMs,
|
|
110
|
-
version,
|
|
111
|
-
etag,
|
|
112
|
-
lastModified: timestamp,
|
|
113
|
-
};
|
|
114
|
-
metadataStore.put(providersMetadata);
|
|
115
121
|
});
|
|
116
122
|
}
|
|
117
123
|
async getHerdModules() {
|
|
118
124
|
await this.init();
|
|
119
125
|
if (!this.db)
|
|
120
126
|
throw new Error("Database not initialized");
|
|
127
|
+
// Validate schema before creating transaction
|
|
128
|
+
if (!this.validateDatabaseSchema()) {
|
|
129
|
+
throw new Error("Database schema validation failed - required object stores not found");
|
|
130
|
+
}
|
|
121
131
|
const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readonly");
|
|
122
132
|
return new Promise((resolve, reject) => {
|
|
123
133
|
transaction.onerror = () => reject(transaction.error);
|
|
@@ -164,30 +174,34 @@ export class ScoutCache {
|
|
|
164
174
|
await this.init();
|
|
165
175
|
if (!this.db)
|
|
166
176
|
throw new Error("Database not initialized");
|
|
167
|
-
|
|
177
|
+
// Validate schema before creating transaction
|
|
178
|
+
if (!this.validateDatabaseSchema()) {
|
|
179
|
+
throw new Error("Database schema validation failed - required object stores not found");
|
|
180
|
+
}
|
|
181
|
+
const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readwrite");
|
|
168
182
|
return new Promise((resolve, reject) => {
|
|
169
183
|
transaction.onerror = () => reject(transaction.error);
|
|
170
184
|
transaction.oncomplete = () => resolve();
|
|
171
185
|
const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
|
|
172
186
|
const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
|
|
173
|
-
const providersStore = transaction.objectStore(PROVIDERS_STORE);
|
|
174
187
|
herdModulesStore.clear();
|
|
175
|
-
providersStore.clear();
|
|
176
188
|
metadataStore.delete("herd_modules");
|
|
177
|
-
metadataStore.delete("providers");
|
|
178
189
|
});
|
|
179
190
|
}
|
|
180
191
|
async invalidateHerdModules() {
|
|
181
192
|
await this.init();
|
|
182
193
|
if (!this.db)
|
|
183
194
|
throw new Error("Database not initialized");
|
|
195
|
+
// Validate schema before creating transaction
|
|
196
|
+
if (!this.validateDatabaseSchema()) {
|
|
197
|
+
throw new Error("Database schema validation failed - required object stores not found");
|
|
198
|
+
}
|
|
184
199
|
const transaction = this.db.transaction([CACHE_METADATA_STORE], "readwrite");
|
|
185
200
|
return new Promise((resolve, reject) => {
|
|
186
201
|
transaction.onerror = () => reject(transaction.error);
|
|
187
202
|
transaction.oncomplete = () => resolve();
|
|
188
203
|
const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
|
|
189
204
|
metadataStore.delete("herd_modules");
|
|
190
|
-
metadataStore.delete("providers");
|
|
191
205
|
});
|
|
192
206
|
}
|
|
193
207
|
async getCacheStats() {
|
|
@@ -248,84 +262,63 @@ export class ScoutCache {
|
|
|
248
262
|
console.warn("[ScoutCache] Background preload failed:", error);
|
|
249
263
|
}
|
|
250
264
|
}
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!this.db)
|
|
255
|
-
throw new Error("Database not initialized");
|
|
256
|
-
const transaction = this.db.transaction([PROVIDERS_STORE, CACHE_METADATA_STORE], "readonly");
|
|
257
|
-
return new Promise((resolve, reject) => {
|
|
258
|
-
transaction.onerror = () => reject(transaction.error);
|
|
259
|
-
const providersStore = transaction.objectStore(PROVIDERS_STORE);
|
|
260
|
-
const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
|
|
261
|
-
// Get metadata first
|
|
262
|
-
const metadataRequest = metadataStore.get("providers");
|
|
263
|
-
metadataRequest.onsuccess = () => {
|
|
264
|
-
const metadata = metadataRequest.result;
|
|
265
|
-
const now = Date.now();
|
|
266
|
-
if (!metadata) {
|
|
267
|
-
this.stats.misses++;
|
|
268
|
-
resolve({ data: null, isStale: true, age: 0, metadata: null });
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const age = now - metadata.timestamp;
|
|
272
|
-
const isStale = age > metadata.ttl;
|
|
273
|
-
// Get providers for specific herd
|
|
274
|
-
const getRequest = providersStore.get(herdId);
|
|
275
|
-
getRequest.onsuccess = () => {
|
|
276
|
-
const cacheEntry = getRequest.result;
|
|
277
|
-
const providers = cacheEntry?.data || [];
|
|
278
|
-
// Update stats
|
|
279
|
-
if (providers.length > 0) {
|
|
280
|
-
this.stats.hits++;
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
this.stats.misses++;
|
|
284
|
-
}
|
|
285
|
-
resolve({
|
|
286
|
-
data: providers,
|
|
287
|
-
isStale,
|
|
288
|
-
age,
|
|
289
|
-
metadata,
|
|
290
|
-
});
|
|
291
|
-
};
|
|
292
|
-
};
|
|
293
|
-
});
|
|
265
|
+
// Get the default TTL value
|
|
266
|
+
getDefaultTtl() {
|
|
267
|
+
return DEFAULT_TTL_MS;
|
|
294
268
|
}
|
|
295
|
-
// Method to
|
|
296
|
-
async
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
269
|
+
// Method to reset the database in case of corruption
|
|
270
|
+
async resetDatabase() {
|
|
271
|
+
console.log("[ScoutCache] Resetting database...");
|
|
272
|
+
// Close existing connection
|
|
273
|
+
if (this.db) {
|
|
274
|
+
this.db.close();
|
|
275
|
+
this.db = null;
|
|
276
|
+
}
|
|
277
|
+
this.initPromise = null;
|
|
278
|
+
// Delete the database
|
|
301
279
|
return new Promise((resolve, reject) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const timestamp = Date.now();
|
|
307
|
-
const version = "1.0.0";
|
|
308
|
-
// Store providers
|
|
309
|
-
const providersCacheEntry = {
|
|
310
|
-
herdId,
|
|
311
|
-
data: providers,
|
|
312
|
-
timestamp,
|
|
280
|
+
const deleteRequest = indexedDB.deleteDatabase(DB_NAME);
|
|
281
|
+
deleteRequest.onsuccess = () => {
|
|
282
|
+
console.log("[ScoutCache] Database reset successfully");
|
|
283
|
+
resolve();
|
|
313
284
|
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
lastModified: timestamp,
|
|
285
|
+
deleteRequest.onerror = () => {
|
|
286
|
+
console.error("[ScoutCache] Failed to reset database:", deleteRequest.error);
|
|
287
|
+
reject(deleteRequest.error);
|
|
288
|
+
};
|
|
289
|
+
deleteRequest.onblocked = () => {
|
|
290
|
+
console.warn("[ScoutCache] Database reset blocked - close all other tabs");
|
|
291
|
+
// Continue anyway, it will resolve when unblocked
|
|
322
292
|
};
|
|
323
|
-
metadataStore.put(metadata);
|
|
324
293
|
});
|
|
325
294
|
}
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
295
|
+
// Method to check database health
|
|
296
|
+
async checkDatabaseHealth() {
|
|
297
|
+
const issues = [];
|
|
298
|
+
try {
|
|
299
|
+
await this.init();
|
|
300
|
+
if (!this.db) {
|
|
301
|
+
issues.push("Database connection not established");
|
|
302
|
+
return { healthy: false, issues };
|
|
303
|
+
}
|
|
304
|
+
if (!this.validateDatabaseSchema()) {
|
|
305
|
+
issues.push("Database schema validation failed");
|
|
306
|
+
}
|
|
307
|
+
// Try a simple read operation
|
|
308
|
+
try {
|
|
309
|
+
await this.getHerdModules();
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
issues.push(`Read operation failed: ${error}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
issues.push(`Database initialization failed: ${error}`);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
healthy: issues.length === 0,
|
|
320
|
+
issues,
|
|
321
|
+
};
|
|
329
322
|
}
|
|
330
323
|
}
|
|
331
324
|
// Singleton instance
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IWebResponseCompatible } from "../types/requests";
|
|
2
|
+
import { IHeartbeat } from "../types/db";
|
|
3
|
+
export declare function server_get_last_heartbeat_by_device(device_id: number): Promise<IWebResponseCompatible<IHeartbeat | null>>;
|
|
4
|
+
export declare function server_get_heartbeats_by_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<IHeartbeat[]>>;
|
|
5
|
+
export declare function server_check_device_online_status(device_id: number, offline_threshold_minutes?: number): Promise<IWebResponseCompatible<{
|
|
6
|
+
is_online: boolean;
|
|
7
|
+
last_heartbeat: IHeartbeat | null;
|
|
8
|
+
minutes_since_last_heartbeat: number | null;
|
|
9
|
+
}>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { newServerClient } from "../supabase/server";
|
|
3
|
+
import { EnumWebResponse, IWebResponse, } from "../types/requests";
|
|
4
|
+
// Function to get the last heartbeat for a device
|
|
5
|
+
export async function server_get_last_heartbeat_by_device(device_id) {
|
|
6
|
+
const supabase = await newServerClient();
|
|
7
|
+
const { data, error } = await supabase
|
|
8
|
+
.from("heartbeats")
|
|
9
|
+
.select("*")
|
|
10
|
+
.eq("device_id", device_id)
|
|
11
|
+
.order("timestamp", { ascending: false })
|
|
12
|
+
.limit(1)
|
|
13
|
+
.single();
|
|
14
|
+
if (error) {
|
|
15
|
+
// If no heartbeats found, return null data instead of error
|
|
16
|
+
if (error.code === "PGRST116") {
|
|
17
|
+
return {
|
|
18
|
+
status: EnumWebResponse.SUCCESS,
|
|
19
|
+
msg: null,
|
|
20
|
+
data: null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
status: EnumWebResponse.ERROR,
|
|
25
|
+
msg: error.message,
|
|
26
|
+
data: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return IWebResponse.success(data).to_compatible();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Function to get all heartbeats for a device
|
|
34
|
+
export async function server_get_heartbeats_by_device(device_id, limit) {
|
|
35
|
+
const supabase = await newServerClient();
|
|
36
|
+
let query = supabase
|
|
37
|
+
.from("heartbeats")
|
|
38
|
+
.select("*")
|
|
39
|
+
.eq("device_id", device_id)
|
|
40
|
+
.order("timestamp", { ascending: false });
|
|
41
|
+
if (limit) {
|
|
42
|
+
query = query.limit(limit);
|
|
43
|
+
}
|
|
44
|
+
const { data, error } = await query;
|
|
45
|
+
if (error) {
|
|
46
|
+
return {
|
|
47
|
+
status: EnumWebResponse.ERROR,
|
|
48
|
+
msg: error.message,
|
|
49
|
+
data: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
return IWebResponse.success(data || []).to_compatible();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Function to check if a device is online based on heartbeat recency
|
|
57
|
+
export async function server_check_device_online_status(device_id, offline_threshold_minutes = 5) {
|
|
58
|
+
const supabase = await newServerClient();
|
|
59
|
+
const { data, error } = await supabase
|
|
60
|
+
.from("heartbeats")
|
|
61
|
+
.select("*")
|
|
62
|
+
.eq("device_id", device_id)
|
|
63
|
+
.order("timestamp", { ascending: false })
|
|
64
|
+
.limit(1)
|
|
65
|
+
.single();
|
|
66
|
+
if (error) {
|
|
67
|
+
// If no heartbeats found
|
|
68
|
+
if (error.code === "PGRST116") {
|
|
69
|
+
return IWebResponse.success({
|
|
70
|
+
is_online: false,
|
|
71
|
+
last_heartbeat: null,
|
|
72
|
+
minutes_since_last_heartbeat: null,
|
|
73
|
+
}).to_compatible();
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
status: EnumWebResponse.ERROR,
|
|
77
|
+
msg: error.message,
|
|
78
|
+
data: null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const now = new Date();
|
|
82
|
+
const heartbeatTime = new Date(data.timestamp);
|
|
83
|
+
const minutesSinceLastHeartbeat = Math.floor((now.getTime() - heartbeatTime.getTime()) / (1000 * 60));
|
|
84
|
+
const isOnline = minutesSinceLastHeartbeat <= offline_threshold_minutes;
|
|
85
|
+
return IWebResponse.success({
|
|
86
|
+
is_online: isOnline,
|
|
87
|
+
last_heartbeat: data,
|
|
88
|
+
minutes_since_last_heartbeat: minutesSinceLastHeartbeat,
|
|
89
|
+
}).to_compatible();
|
|
90
|
+
}
|
package/dist/helpers/index.d.ts
CHANGED
package/dist/helpers/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CacheStats, TimingStats } from "../helpers/cache";
|
|
1
|
+
import { CacheStats, TimingStats, DatabaseHealth } from "../helpers/cache";
|
|
2
2
|
export interface UseScoutRefreshOptions {
|
|
3
3
|
autoRefresh?: boolean;
|
|
4
4
|
onRefreshComplete?: () => void;
|
|
@@ -41,4 +41,6 @@ export declare function useScoutRefresh(options?: UseScoutRefreshOptions): {
|
|
|
41
41
|
getTimingStats: () => TimingStats;
|
|
42
42
|
clearCache: () => Promise<void>;
|
|
43
43
|
getCacheStats: () => Promise<CacheStats>;
|
|
44
|
+
checkDatabaseHealth: () => Promise<DatabaseHealth>;
|
|
45
|
+
resetDatabase: () => Promise<void>;
|
|
44
46
|
};
|
|
@@ -4,7 +4,7 @@ import { EnumScoutStateStatus, setActiveHerdId, setHerdModules, setStatus, setHe
|
|
|
4
4
|
import { EnumHerdModulesLoadingState } from "../types/herd_module";
|
|
5
5
|
import { server_load_herd_modules } from "../helpers/herds";
|
|
6
6
|
import { server_get_user } from "../helpers/users";
|
|
7
|
-
import { scoutCache } from "../helpers/cache";
|
|
7
|
+
import { scoutCache, } from "../helpers/cache";
|
|
8
8
|
import { EnumDataSource } from "../types/data_source";
|
|
9
9
|
/**
|
|
10
10
|
* Hook for refreshing scout data with detailed timing measurements and cache-first loading
|
|
@@ -107,7 +107,7 @@ export function useScoutRefresh(options = {}) {
|
|
|
107
107
|
const backgroundStartTime = Date.now();
|
|
108
108
|
const [backgroundHerdModulesResult, backgroundUserResult] = await Promise.all([
|
|
109
109
|
server_load_herd_modules(),
|
|
110
|
-
server_get_user()
|
|
110
|
+
server_get_user(),
|
|
111
111
|
]);
|
|
112
112
|
const backgroundDuration = Date.now() - backgroundStartTime;
|
|
113
113
|
console.log(`[useScoutRefresh] Background fetch completed in ${backgroundDuration}ms`);
|
|
@@ -123,6 +123,21 @@ export function useScoutRefresh(options = {}) {
|
|
|
123
123
|
}
|
|
124
124
|
catch (cacheError) {
|
|
125
125
|
console.warn("[useScoutRefresh] Background cache save failed:", cacheError);
|
|
126
|
+
// If it's an IndexedDB object store error, try to reset the database
|
|
127
|
+
if (cacheError instanceof Error &&
|
|
128
|
+
(cacheError.message.includes("object store") ||
|
|
129
|
+
cacheError.message.includes("NotFoundError"))) {
|
|
130
|
+
console.log("[useScoutRefresh] Attempting database reset due to schema error...");
|
|
131
|
+
try {
|
|
132
|
+
await scoutCache.resetDatabase();
|
|
133
|
+
console.log("[useScoutRefresh] Database reset successful, retrying cache save...");
|
|
134
|
+
await scoutCache.setHerdModules(backgroundHerdModulesResult.data, cacheTtlMs);
|
|
135
|
+
console.log("[useScoutRefresh] Cache save successful after database reset");
|
|
136
|
+
}
|
|
137
|
+
catch (resetError) {
|
|
138
|
+
console.error("[useScoutRefresh] Database reset and retry failed:", resetError);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
126
141
|
}
|
|
127
142
|
// Update store with fresh data
|
|
128
143
|
dispatch(setHerdModules(backgroundHerdModulesResult.data));
|
|
@@ -157,6 +172,19 @@ export function useScoutRefresh(options = {}) {
|
|
|
157
172
|
}
|
|
158
173
|
catch (cacheError) {
|
|
159
174
|
console.warn("[useScoutRefresh] Cache load failed:", cacheError);
|
|
175
|
+
// If it's an IndexedDB object store error, try to reset the database
|
|
176
|
+
if (cacheError instanceof Error &&
|
|
177
|
+
(cacheError.message.includes("object store") ||
|
|
178
|
+
cacheError.message.includes("NotFoundError"))) {
|
|
179
|
+
console.log("[useScoutRefresh] Attempting database reset due to cache load error...");
|
|
180
|
+
try {
|
|
181
|
+
await scoutCache.resetDatabase();
|
|
182
|
+
console.log("[useScoutRefresh] Database reset successful");
|
|
183
|
+
}
|
|
184
|
+
catch (resetError) {
|
|
185
|
+
console.error("[useScoutRefresh] Database reset failed:", resetError);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
160
188
|
// Continue with API call
|
|
161
189
|
}
|
|
162
190
|
}
|
|
@@ -223,6 +251,21 @@ export function useScoutRefresh(options = {}) {
|
|
|
223
251
|
}
|
|
224
252
|
catch (cacheError) {
|
|
225
253
|
console.warn("[useScoutRefresh] Cache save failed:", cacheError);
|
|
254
|
+
// If it's an IndexedDB object store error, try to reset the database
|
|
255
|
+
if (cacheError instanceof Error &&
|
|
256
|
+
(cacheError.message.includes("object store") ||
|
|
257
|
+
cacheError.message.includes("NotFoundError"))) {
|
|
258
|
+
console.log("[useScoutRefresh] Attempting database reset due to cache save error...");
|
|
259
|
+
try {
|
|
260
|
+
await scoutCache.resetDatabase();
|
|
261
|
+
console.log("[useScoutRefresh] Database reset successful, retrying cache save...");
|
|
262
|
+
await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs);
|
|
263
|
+
console.log("[useScoutRefresh] Cache save successful after database reset");
|
|
264
|
+
}
|
|
265
|
+
catch (resetError) {
|
|
266
|
+
console.error("[useScoutRefresh] Database reset and retry failed:", resetError);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
226
269
|
}
|
|
227
270
|
// Step 4: Update store with fresh data
|
|
228
271
|
const dataProcessingStartTime = Date.now();
|
|
@@ -345,10 +388,36 @@ export function useScoutRefresh(options = {}) {
|
|
|
345
388
|
};
|
|
346
389
|
}
|
|
347
390
|
}, []);
|
|
391
|
+
// Utility function to check database health
|
|
392
|
+
const checkDatabaseHealth = useCallback(async () => {
|
|
393
|
+
try {
|
|
394
|
+
return await scoutCache.checkDatabaseHealth();
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
console.error("[useScoutRefresh] Failed to check database health:", error);
|
|
398
|
+
return {
|
|
399
|
+
healthy: false,
|
|
400
|
+
issues: [`Health check failed: ${error}`],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}, []);
|
|
404
|
+
// Utility function to reset database
|
|
405
|
+
const resetDatabase = useCallback(async () => {
|
|
406
|
+
try {
|
|
407
|
+
await scoutCache.resetDatabase();
|
|
408
|
+
console.log("[useScoutRefresh] Database reset successfully");
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error("[useScoutRefresh] Failed to reset database:", error);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}, []);
|
|
348
415
|
return {
|
|
349
416
|
handleRefresh,
|
|
350
417
|
getTimingStats,
|
|
351
418
|
clearCache,
|
|
352
419
|
getCacheStats,
|
|
420
|
+
checkDatabaseHealth,
|
|
421
|
+
resetDatabase,
|
|
353
422
|
};
|
|
354
423
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export * from "./helpers/web";
|
|
|
30
30
|
export * from "./helpers/zones";
|
|
31
31
|
export * from "./helpers/storage";
|
|
32
32
|
export * from "./helpers/eventUtils";
|
|
33
|
+
export * from "./helpers/cache";
|
|
33
34
|
export * from "./hooks/useScoutDbListener";
|
|
34
35
|
export * from "./hooks/useScoutRefresh";
|
|
35
36
|
export * from "./providers";
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./helpers/web";
|
|
|
33
33
|
export * from "./helpers/zones";
|
|
34
34
|
export * from "./helpers/storage";
|
|
35
35
|
export * from "./helpers/eventUtils";
|
|
36
|
+
export * from "./helpers/cache";
|
|
36
37
|
// Hooks
|
|
37
38
|
export * from "./hooks/useScoutDbListener";
|
|
38
39
|
export * from "./hooks/useScoutRefresh";
|
|
@@ -289,6 +289,33 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
|
|
|
289
289
|
referencedColumns: ["id"];
|
|
290
290
|
}];
|
|
291
291
|
};
|
|
292
|
+
heartbeats: {
|
|
293
|
+
Row: {
|
|
294
|
+
created_at: string;
|
|
295
|
+
device_id: number;
|
|
296
|
+
id: number;
|
|
297
|
+
timestamp: string;
|
|
298
|
+
};
|
|
299
|
+
Insert: {
|
|
300
|
+
created_at?: string;
|
|
301
|
+
device_id: number;
|
|
302
|
+
id?: number;
|
|
303
|
+
timestamp: string;
|
|
304
|
+
};
|
|
305
|
+
Update: {
|
|
306
|
+
created_at?: string;
|
|
307
|
+
device_id?: number;
|
|
308
|
+
id?: number;
|
|
309
|
+
timestamp?: string;
|
|
310
|
+
};
|
|
311
|
+
Relationships: [{
|
|
312
|
+
foreignKeyName: "heartbeats_device_id_fkey";
|
|
313
|
+
columns: ["device_id"];
|
|
314
|
+
isOneToOne: false;
|
|
315
|
+
referencedRelation: "devices";
|
|
316
|
+
referencedColumns: ["id"];
|
|
317
|
+
}];
|
|
318
|
+
};
|
|
292
319
|
herds: {
|
|
293
320
|
Row: {
|
|
294
321
|
created_by: string;
|
package/dist/types/db.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type IUserRolePerHerd = Database["public"]["Tables"]["users_roles_per_her
|
|
|
21
21
|
export type IHerd = Database["public"]["Tables"]["herds"]["Row"];
|
|
22
22
|
export type ISession = Database["public"]["Tables"]["sessions"]["Row"];
|
|
23
23
|
export type IConnectivity = Database["public"]["Tables"]["connectivity"]["Row"];
|
|
24
|
-
export type
|
|
24
|
+
export type IHeartbeat = Database["public"]["Tables"]["heartbeats"]["Row"];
|
|
25
25
|
export type IEventWithTags = Database["public"]["CompositeTypes"]["event_with_tags"] & {
|
|
26
26
|
earthranger_url: string | null;
|
|
27
27
|
file_path: string | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { IDevice, IEventWithTags, IHerd, IPlan, ILayer,
|
|
2
|
+
import { IDevice, IEventWithTags, IHerd, IPlan, ILayer, IUserAndRole, IZoneWithActions, ISessionWithCoordinates } from "../types/db";
|
|
3
3
|
import { EnumWebResponse } from "./requests";
|
|
4
4
|
export declare enum EnumHerdModulesLoadingState {
|
|
5
5
|
NOT_LOADING = "NOT_LOADING",
|
|
@@ -21,8 +21,7 @@ export declare class HerdModule {
|
|
|
21
21
|
labels: string[];
|
|
22
22
|
plans: IPlan[];
|
|
23
23
|
layers: ILayer[];
|
|
24
|
-
|
|
25
|
-
constructor(herd: IHerd, devices: IDevice[], events: IEventWithTags[], timestamp_last_refreshed: number, user_roles?: IUserAndRole[] | null, events_page_index?: number, total_events?: number, total_events_with_filters?: number, labels?: string[], plans?: IPlan[], zones?: IZoneWithActions[], sessions?: ISessionWithCoordinates[], layers?: ILayer[], providers?: IProvider[]);
|
|
24
|
+
constructor(herd: IHerd, devices: IDevice[], events: IEventWithTags[], timestamp_last_refreshed: number, user_roles?: IUserAndRole[] | null, events_page_index?: number, total_events?: number, total_events_with_filters?: number, labels?: string[], plans?: IPlan[], zones?: IZoneWithActions[], sessions?: ISessionWithCoordinates[], layers?: ILayer[]);
|
|
26
25
|
to_serializable(): IHerdModule;
|
|
27
26
|
static from_herd(herd: IHerd, client: SupabaseClient): Promise<HerdModule>;
|
|
28
27
|
}
|
|
@@ -40,7 +39,6 @@ export interface IHerdModule {
|
|
|
40
39
|
zones: IZoneWithActions[];
|
|
41
40
|
sessions: ISessionWithCoordinates[];
|
|
42
41
|
layers: ILayer[];
|
|
43
|
-
providers: IProvider[];
|
|
44
42
|
}
|
|
45
43
|
export interface IHerdModulesResponse {
|
|
46
44
|
data: IHerdModule[];
|
|
@@ -4,7 +4,6 @@ import { server_get_total_events_by_herd } from "../helpers/events";
|
|
|
4
4
|
import { EnumSessionsVisibility } from "./events";
|
|
5
5
|
import { server_get_plans_by_herd } from "../helpers/plans";
|
|
6
6
|
import { server_get_layers_by_herd } from "../helpers/layers";
|
|
7
|
-
import { server_get_providers_by_herd } from "../helpers/providers";
|
|
8
7
|
import { server_get_events_and_tags_for_devices_batch } from "../helpers/tags";
|
|
9
8
|
import { server_get_users_with_herd_access } from "../helpers/users";
|
|
10
9
|
import { EnumWebResponse } from "./requests";
|
|
@@ -19,7 +18,7 @@ export var EnumHerdModulesLoadingState;
|
|
|
19
18
|
EnumHerdModulesLoadingState["UNSUCCESSFULLY_LOADED"] = "UNSUCCESSFULLY_LOADED";
|
|
20
19
|
})(EnumHerdModulesLoadingState || (EnumHerdModulesLoadingState = {}));
|
|
21
20
|
export class HerdModule {
|
|
22
|
-
constructor(herd, devices, events, timestamp_last_refreshed, user_roles = null, events_page_index = 0, total_events = 0, total_events_with_filters = 0, labels = [], plans = [], zones = [], sessions = [], layers = []
|
|
21
|
+
constructor(herd, devices, events, timestamp_last_refreshed, user_roles = null, events_page_index = 0, total_events = 0, total_events_with_filters = 0, labels = [], plans = [], zones = [], sessions = [], layers = []) {
|
|
23
22
|
this.user_roles = null;
|
|
24
23
|
this.events_page_index = 0;
|
|
25
24
|
this.total_events = 0;
|
|
@@ -27,7 +26,6 @@ export class HerdModule {
|
|
|
27
26
|
this.labels = [];
|
|
28
27
|
this.plans = [];
|
|
29
28
|
this.layers = [];
|
|
30
|
-
this.providers = [];
|
|
31
29
|
this.herd = herd;
|
|
32
30
|
this.devices = devices;
|
|
33
31
|
this.events = events;
|
|
@@ -41,7 +39,6 @@ export class HerdModule {
|
|
|
41
39
|
this.zones = zones;
|
|
42
40
|
this.sessions = sessions;
|
|
43
41
|
this.layers = layers;
|
|
44
|
-
this.providers = providers;
|
|
45
42
|
}
|
|
46
43
|
to_serializable() {
|
|
47
44
|
return {
|
|
@@ -58,7 +55,6 @@ export class HerdModule {
|
|
|
58
55
|
zones: this.zones,
|
|
59
56
|
sessions: this.sessions,
|
|
60
57
|
layers: this.layers,
|
|
61
|
-
providers: this.providers,
|
|
62
58
|
};
|
|
63
59
|
}
|
|
64
60
|
static async from_herd(herd, client) {
|
|
@@ -99,7 +95,7 @@ export class HerdModule {
|
|
|
99
95
|
}
|
|
100
96
|
}
|
|
101
97
|
// Run all remaining requests in parallel with individual error handling
|
|
102
|
-
const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers,
|
|
98
|
+
const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers,] = await Promise.allSettled([
|
|
103
99
|
server_get_more_zones_and_actions_for_herd(herd.id, 0, 10).catch((error) => {
|
|
104
100
|
console.warn(`[HerdModule] Failed to get zones and actions:`, error);
|
|
105
101
|
return { status: EnumWebResponse.ERROR, data: null };
|
|
@@ -118,20 +114,12 @@ export class HerdModule {
|
|
|
118
114
|
}),
|
|
119
115
|
server_get_sessions_by_herd_id(herd.id).catch((error) => {
|
|
120
116
|
console.warn(`[HerdModule] Failed to get sessions:`, error);
|
|
121
|
-
return {
|
|
122
|
-
status: EnumWebResponse.ERROR,
|
|
123
|
-
data: [],
|
|
124
|
-
msg: error.message,
|
|
125
|
-
};
|
|
117
|
+
return { status: EnumWebResponse.ERROR, data: [], msg: error.message };
|
|
126
118
|
}),
|
|
127
119
|
server_get_layers_by_herd(herd.id).catch((error) => {
|
|
128
120
|
console.warn(`[HerdModule] Failed to get layers:`, error);
|
|
129
121
|
return { status: EnumWebResponse.ERROR, data: null };
|
|
130
122
|
}),
|
|
131
|
-
server_get_providers_by_herd(herd.id).catch((error) => {
|
|
132
|
-
console.warn(`[HerdModule] Failed to get providers:`, error);
|
|
133
|
-
return { status: EnumWebResponse.ERROR, data: null };
|
|
134
|
-
}),
|
|
135
123
|
]);
|
|
136
124
|
// Assign recent events to devices from batch results
|
|
137
125
|
for (let i = 0; i < new_devices.length; i++) {
|
|
@@ -160,28 +148,23 @@ export class HerdModule {
|
|
|
160
148
|
const plans = res_plans.status === "fulfilled" && res_plans.value?.data
|
|
161
149
|
? res_plans.value.data
|
|
162
150
|
: [];
|
|
163
|
-
const sessions = res_sessions.status === "fulfilled" && res_sessions.value?.data
|
|
164
|
-
? res_sessions.value.data
|
|
165
|
-
: [];
|
|
151
|
+
const sessions = res_sessions.status === "fulfilled" && res_sessions.value?.data ? res_sessions.value.data : [];
|
|
166
152
|
const layers = res_layers.status === "fulfilled" && res_layers.value?.data
|
|
167
153
|
? res_layers.value.data
|
|
168
154
|
: [];
|
|
169
|
-
const providers = res_providers.status === "fulfilled" && res_providers.value?.data
|
|
170
|
-
? res_providers.value.data
|
|
171
|
-
: [];
|
|
172
155
|
// TODO: store in DB and retrieve on load?
|
|
173
156
|
const newLabels = LABELS;
|
|
174
157
|
const endTime = Date.now();
|
|
175
158
|
const loadTime = endTime - startTime;
|
|
176
159
|
console.log(`[HerdModule] Loaded herd ${herd.slug} in ${loadTime}ms (${new_devices.length} devices)`);
|
|
177
|
-
return new HerdModule(herd, new_devices, [], Date.now(), user_roles, 0, total_events, total_events, newLabels, plans, zones, sessions, layers
|
|
160
|
+
return new HerdModule(herd, new_devices, [], Date.now(), user_roles, 0, total_events, total_events, newLabels, plans, zones, sessions, layers);
|
|
178
161
|
}
|
|
179
162
|
catch (error) {
|
|
180
163
|
const endTime = Date.now();
|
|
181
164
|
const loadTime = endTime - startTime;
|
|
182
165
|
console.error(`[HerdModule] Critical error in HerdModule.from_herd (${loadTime}ms):`, error);
|
|
183
166
|
// Return a minimal but valid HerdModule instance to prevent complete failure
|
|
184
|
-
return new HerdModule(herd, [], [], Date.now(), null, 0, 0, 0, [], [], [], [], []
|
|
167
|
+
return new HerdModule(herd, [], [], Date.now(), null, 0, 0, 0, [], [], [], [], []);
|
|
185
168
|
}
|
|
186
169
|
}
|
|
187
170
|
}
|
package/dist/types/supabase.d.ts
CHANGED
|
@@ -302,6 +302,35 @@ export type Database = {
|
|
|
302
302
|
}
|
|
303
303
|
];
|
|
304
304
|
};
|
|
305
|
+
heartbeats: {
|
|
306
|
+
Row: {
|
|
307
|
+
created_at: string;
|
|
308
|
+
device_id: number;
|
|
309
|
+
id: number;
|
|
310
|
+
timestamp: string;
|
|
311
|
+
};
|
|
312
|
+
Insert: {
|
|
313
|
+
created_at?: string;
|
|
314
|
+
device_id: number;
|
|
315
|
+
id?: number;
|
|
316
|
+
timestamp: string;
|
|
317
|
+
};
|
|
318
|
+
Update: {
|
|
319
|
+
created_at?: string;
|
|
320
|
+
device_id?: number;
|
|
321
|
+
id?: number;
|
|
322
|
+
timestamp?: string;
|
|
323
|
+
};
|
|
324
|
+
Relationships: [
|
|
325
|
+
{
|
|
326
|
+
foreignKeyName: "heartbeats_device_id_fkey";
|
|
327
|
+
columns: ["device_id"];
|
|
328
|
+
isOneToOne: false;
|
|
329
|
+
referencedRelation: "devices";
|
|
330
|
+
referencedColumns: ["id"];
|
|
331
|
+
}
|
|
332
|
+
];
|
|
333
|
+
};
|
|
305
334
|
herds: {
|
|
306
335
|
Row: {
|
|
307
336
|
created_by: string;
|