@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.
@@ -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;
@@ -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
- // Create herd modules store
36
- if (!db.objectStoreNames.contains(HERD_MODULES_STORE)) {
37
- const herdModulesStore = db.createObjectStore(HERD_MODULES_STORE, {
38
- keyPath: "herdId",
39
- });
40
- herdModulesStore.createIndex("timestamp", "timestamp", {
41
- unique: false,
42
- });
43
- }
44
- // Create cache metadata store
45
- if (!db.objectStoreNames.contains(CACHE_METADATA_STORE)) {
46
- const metadataStore = db.createObjectStore(CACHE_METADATA_STORE, {
47
- keyPath: "key",
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
- // Create providers store
51
- if (!db.objectStoreNames.contains(PROVIDERS_STORE)) {
52
- const providersStore = db.createObjectStore(PROVIDERS_STORE, {
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
- const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE, PROVIDERS_STORE], "readwrite");
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 and its providers
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
- const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE, PROVIDERS_STORE], "readwrite");
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
- // Method to get providers for a specific herd from cache
252
- async getProvidersForHerd(herdId) {
253
- await this.init();
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 set providers for a specific herd
296
- async setProvidersForHerd(herdId, providers, ttlMs = DEFAULT_TTL_MS) {
297
- await this.init();
298
- if (!this.db)
299
- throw new Error("Database not initialized");
300
- const transaction = this.db.transaction([PROVIDERS_STORE, CACHE_METADATA_STORE], "readwrite");
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
- transaction.onerror = () => reject(transaction.error);
303
- transaction.oncomplete = () => resolve();
304
- const providersStore = transaction.objectStore(PROVIDERS_STORE);
305
- const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
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
- providersStore.put(providersCacheEntry);
315
- // Update providers metadata
316
- const metadata = {
317
- key: "providers",
318
- timestamp,
319
- ttl: ttlMs,
320
- version,
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
- // Get the default TTL value
327
- getDefaultTtl() {
328
- return DEFAULT_TTL_MS;
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
+ }
@@ -6,6 +6,7 @@ export * from "./devices";
6
6
  export * from "./email";
7
7
  export * from "./events";
8
8
  export * from "./gps";
9
+ export * from "./heartbeats";
9
10
  export * from "./herds";
10
11
  export * from "./location";
11
12
  export * from "./plans";
@@ -6,6 +6,7 @@ export * from "./devices";
6
6
  export * from "./email";
7
7
  export * from "./events";
8
8
  export * from "./gps";
9
+ export * from "./heartbeats";
9
10
  export * from "./herds";
10
11
  export * from "./location";
11
12
  export * from "./plans";
@@ -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;
@@ -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 IProvider = Database["public"]["Tables"]["providers"]["Row"];
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, IProvider, IUserAndRole, IZoneWithActions, ISessionWithCoordinates } from "../types/db";
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
- providers: IProvider[];
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 = [], providers = []) {
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, res_providers,] = await Promise.allSettled([
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, providers);
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
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.83",
3
+ "version": "1.0.85",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",