@adventurelabs/scout-core 1.0.85 → 1.0.86

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.
@@ -4,6 +4,7 @@ export interface CacheMetadata {
4
4
  timestamp: number;
5
5
  ttl: number;
6
6
  version: string;
7
+ dbVersion: number;
7
8
  etag?: string;
8
9
  lastModified?: number;
9
10
  }
@@ -53,6 +54,8 @@ export declare class ScoutCache {
53
54
  }>;
54
55
  preloadCache(loadFunction: () => Promise<IHerdModule[]>, ttlMs?: number): Promise<void>;
55
56
  getDefaultTtl(): number;
57
+ getCurrentDbVersion(): number;
58
+ isCacheVersionCompatible(): Promise<boolean>;
56
59
  resetDatabase(): Promise<void>;
57
60
  checkDatabaseHealth(): Promise<DatabaseHealth>;
58
61
  }
@@ -1,5 +1,5 @@
1
1
  const DB_NAME = "ScoutCache";
2
- const DB_VERSION = 1;
2
+ const DB_VERSION = 2; // Increment to invalidate old cache versions
3
3
  const HERD_MODULES_STORE = "herd_modules";
4
4
  const CACHE_METADATA_STORE = "cache_metadata";
5
5
  // Default TTL: 24 hours (1 day)
@@ -28,45 +28,49 @@ export class ScoutCache {
28
28
  };
29
29
  request.onsuccess = () => {
30
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
- }
40
31
  console.log("[ScoutCache] IndexedDB initialized successfully");
32
+ // Add error handler for runtime database errors
33
+ this.db.onerror = (event) => {
34
+ console.error("[ScoutCache] Database error:", event);
35
+ };
41
36
  resolve();
42
37
  };
43
38
  request.onupgradeneeded = (event) => {
44
39
  const db = event.target.result;
45
40
  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");
41
+ console.log(`[ScoutCache] Upgrading database to version ${DB_VERSION}`);
42
+ // Remove all existing object stores to ensure clean slate
43
+ const existingStores = Array.from(db.objectStoreNames);
44
+ for (const storeName of existingStores) {
45
+ console.log(`[ScoutCache] Removing existing object store: ${storeName}`);
46
+ db.deleteObjectStore(storeName);
55
47
  }
48
+ // Create herd modules store (unified storage for all herd data)
49
+ const herdModulesStore = db.createObjectStore(HERD_MODULES_STORE, {
50
+ keyPath: "herdId",
51
+ });
52
+ herdModulesStore.createIndex("timestamp", "timestamp", {
53
+ unique: false,
54
+ });
55
+ herdModulesStore.createIndex("dbVersion", "dbVersion", {
56
+ unique: false,
57
+ });
58
+ console.log("[ScoutCache] Created herd_modules object store");
56
59
  // 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");
60
+ const metadataStore = db.createObjectStore(CACHE_METADATA_STORE, {
61
+ keyPath: "key",
62
+ });
63
+ console.log("[ScoutCache] Created cache_metadata object store");
64
+ console.log(`[ScoutCache] Database schema upgrade to version ${DB_VERSION} completed`);
64
65
  }
65
66
  catch (error) {
66
67
  console.error("[ScoutCache] Error during database upgrade:", error);
67
68
  reject(error);
68
69
  }
69
70
  };
71
+ request.onblocked = () => {
72
+ console.warn("[ScoutCache] Database upgrade blocked - other connections may need to be closed");
73
+ };
70
74
  });
71
75
  return this.initPromise;
72
76
  }
@@ -87,7 +91,6 @@ export class ScoutCache {
87
91
  await this.init();
88
92
  if (!this.db)
89
93
  throw new Error("Database not initialized");
90
- // Validate schema before creating transaction
91
94
  if (!this.validateDatabaseSchema()) {
92
95
  throw new Error("Database schema validation failed - required object stores not found");
93
96
  }
@@ -98,13 +101,14 @@ export class ScoutCache {
98
101
  const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
99
102
  const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
100
103
  const timestamp = Date.now();
101
- const version = "1.0.0";
102
- // Store each herd module
104
+ const version = "2.0.0";
105
+ // Store each herd module (contains all nested data - devices, events, zones, etc.)
103
106
  herdModules.forEach((herdModule) => {
104
107
  const cacheEntry = {
105
108
  herdId: herdModule.herd.id.toString(),
106
109
  data: herdModule,
107
110
  timestamp,
111
+ dbVersion: DB_VERSION,
108
112
  };
109
113
  herdModulesStore.put(cacheEntry);
110
114
  });
@@ -114,6 +118,7 @@ export class ScoutCache {
114
118
  timestamp,
115
119
  ttl: ttlMs,
116
120
  version,
121
+ dbVersion: DB_VERSION,
117
122
  etag,
118
123
  lastModified: timestamp,
119
124
  };
@@ -124,7 +129,6 @@ export class ScoutCache {
124
129
  await this.init();
125
130
  if (!this.db)
126
131
  throw new Error("Database not initialized");
127
- // Validate schema before creating transaction
128
132
  if (!this.validateDatabaseSchema()) {
129
133
  throw new Error("Database schema validation failed - required object stores not found");
130
134
  }
@@ -143,6 +147,17 @@ export class ScoutCache {
143
147
  resolve({ data: null, isStale: true, age: 0, metadata: null });
144
148
  return;
145
149
  }
150
+ // Check if cache is from an incompatible DB version
151
+ if (!metadata.dbVersion || metadata.dbVersion !== DB_VERSION) {
152
+ console.log(`[ScoutCache] Cache from incompatible DB version (${metadata.dbVersion || "unknown"} !== ${DB_VERSION}), invalidating`);
153
+ this.stats.misses++;
154
+ // Clear old cache asynchronously
155
+ this.clearHerdModules().catch((error) => {
156
+ console.warn("[ScoutCache] Failed to clear old cache:", error);
157
+ });
158
+ resolve({ data: null, isStale: true, age: 0, metadata: null });
159
+ return;
160
+ }
146
161
  const age = now - metadata.timestamp;
147
162
  const isStale = age > metadata.ttl;
148
163
  // Get all herd modules
@@ -150,7 +165,10 @@ export class ScoutCache {
150
165
  getAllRequest.onsuccess = () => {
151
166
  const cacheEntries = getAllRequest.result;
152
167
  const herdModules = cacheEntries
153
- .filter((entry) => entry.data && entry.data.herd && entry.data.herd.slug)
168
+ .filter((entry) => entry.data &&
169
+ entry.data.herd &&
170
+ entry.data.herd.slug &&
171
+ entry.dbVersion === DB_VERSION)
154
172
  .map((entry) => entry.data)
155
173
  .sort((a, b) => (a.herd?.slug || "").localeCompare(b.herd?.slug || ""));
156
174
  // Update stats
@@ -174,7 +192,6 @@ export class ScoutCache {
174
192
  await this.init();
175
193
  if (!this.db)
176
194
  throw new Error("Database not initialized");
177
- // Validate schema before creating transaction
178
195
  if (!this.validateDatabaseSchema()) {
179
196
  throw new Error("Database schema validation failed - required object stores not found");
180
197
  }
@@ -192,7 +209,6 @@ export class ScoutCache {
192
209
  await this.init();
193
210
  if (!this.db)
194
211
  throw new Error("Database not initialized");
195
- // Validate schema before creating transaction
196
212
  if (!this.validateDatabaseSchema()) {
197
213
  throw new Error("Database schema validation failed - required object stores not found");
198
214
  }
@@ -202,6 +218,7 @@ export class ScoutCache {
202
218
  transaction.oncomplete = () => resolve();
203
219
  const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
204
220
  metadataStore.delete("herd_modules");
221
+ metadataStore.delete("providers");
205
222
  });
206
223
  }
207
224
  async getCacheStats() {
@@ -228,7 +245,6 @@ export class ScoutCache {
228
245
  const result = await this.getHerdModules();
229
246
  return result.age;
230
247
  }
231
- // Method to check if we should refresh based on various conditions
232
248
  async shouldRefresh(maxAgeMs, forceRefresh) {
233
249
  if (forceRefresh) {
234
250
  return { shouldRefresh: true, reason: "Force refresh requested" };
@@ -237,6 +253,15 @@ export class ScoutCache {
237
253
  if (!result.data || result.data.length === 0) {
238
254
  return { shouldRefresh: true, reason: "No cached data" };
239
255
  }
256
+ // Check for DB version mismatch
257
+ if (!result.metadata ||
258
+ !result.metadata.dbVersion ||
259
+ result.metadata.dbVersion !== DB_VERSION) {
260
+ return {
261
+ shouldRefresh: true,
262
+ reason: `Cache from incompatible DB version (${result.metadata?.dbVersion || "unknown"} !== ${DB_VERSION})`,
263
+ };
264
+ }
240
265
  if (result.isStale) {
241
266
  return { shouldRefresh: true, reason: "Cache is stale" };
242
267
  }
@@ -248,7 +273,6 @@ export class ScoutCache {
248
273
  }
249
274
  return { shouldRefresh: false, reason: "Cache is valid and fresh" };
250
275
  }
251
- // Method to preload cache with background refresh
252
276
  async preloadCache(loadFunction, ttlMs = DEFAULT_TTL_MS) {
253
277
  try {
254
278
  console.log("[ScoutCache] Starting background cache preload...");
@@ -262,11 +286,25 @@ export class ScoutCache {
262
286
  console.warn("[ScoutCache] Background preload failed:", error);
263
287
  }
264
288
  }
265
- // Get the default TTL value
266
289
  getDefaultTtl() {
267
290
  return DEFAULT_TTL_MS;
268
291
  }
269
- // Method to reset the database in case of corruption
292
+ getCurrentDbVersion() {
293
+ return DB_VERSION;
294
+ }
295
+ async isCacheVersionCompatible() {
296
+ try {
297
+ const result = await this.getHerdModules();
298
+ if (!result.metadata)
299
+ return false;
300
+ return (result.metadata.dbVersion !== undefined &&
301
+ result.metadata.dbVersion === DB_VERSION);
302
+ }
303
+ catch (error) {
304
+ console.warn("[ScoutCache] Version compatibility check failed:", error);
305
+ return false;
306
+ }
307
+ }
270
308
  async resetDatabase() {
271
309
  console.log("[ScoutCache] Resetting database...");
272
310
  // Close existing connection
@@ -292,7 +330,6 @@ export class ScoutCache {
292
330
  };
293
331
  });
294
332
  }
295
- // Method to check database health
296
333
  async checkDatabaseHealth() {
297
334
  const issues = [];
298
335
  try {
@@ -304,9 +341,17 @@ export class ScoutCache {
304
341
  if (!this.validateDatabaseSchema()) {
305
342
  issues.push("Database schema validation failed");
306
343
  }
344
+ // Check version compatibility
345
+ const isVersionCompatible = await this.isCacheVersionCompatible();
346
+ if (!isVersionCompatible) {
347
+ issues.push(`Cache version incompatible (current: ${DB_VERSION})`);
348
+ }
307
349
  // Try a simple read operation
308
350
  try {
309
- await this.getHerdModules();
351
+ const result = await this.getHerdModules();
352
+ if (result.data === null && result.age === 0) {
353
+ // This is expected for empty cache, not an error
354
+ }
310
355
  }
311
356
  catch (error) {
312
357
  issues.push(`Read operation failed: ${error}`);
@@ -43,4 +43,6 @@ export declare function useScoutRefresh(options?: UseScoutRefreshOptions): {
43
43
  getCacheStats: () => Promise<CacheStats>;
44
44
  checkDatabaseHealth: () => Promise<DatabaseHealth>;
45
45
  resetDatabase: () => Promise<void>;
46
+ isCacheVersionCompatible: () => Promise<boolean>;
47
+ getCurrentDbVersion: () => number;
46
48
  };
@@ -412,6 +412,20 @@ export function useScoutRefresh(options = {}) {
412
412
  throw error;
413
413
  }
414
414
  }, []);
415
+ // Utility function to check cache version compatibility
416
+ const isCacheVersionCompatible = useCallback(async () => {
417
+ try {
418
+ return await scoutCache.isCacheVersionCompatible();
419
+ }
420
+ catch (error) {
421
+ console.error("[useScoutRefresh] Failed to check cache version compatibility:", error);
422
+ return false;
423
+ }
424
+ }, []);
425
+ // Utility function to get current DB version
426
+ const getCurrentDbVersion = useCallback(() => {
427
+ return scoutCache.getCurrentDbVersion();
428
+ }, []);
415
429
  return {
416
430
  handleRefresh,
417
431
  getTimingStats,
@@ -419,5 +433,7 @@ export function useScoutRefresh(options = {}) {
419
433
  getCacheStats,
420
434
  checkDatabaseHealth,
421
435
  resetDatabase,
436
+ isCacheVersionCompatible,
437
+ getCurrentDbVersion,
422
438
  };
423
439
  }
@@ -22,6 +22,7 @@ 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
24
  export type IHeartbeat = Database["public"]["Tables"]["heartbeats"]["Row"];
25
+ export type IProvider = Database["public"]["Tables"]["providers"]["Row"];
25
26
  export type IEventWithTags = Database["public"]["CompositeTypes"]["event_with_tags"] & {
26
27
  earthranger_url: string | null;
27
28
  file_path: string | null;
@@ -1,5 +1,5 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
- import { IDevice, IEventWithTags, IHerd, IPlan, ILayer, IUserAndRole, IZoneWithActions, ISessionWithCoordinates } from "../types/db";
2
+ import { IDevice, IEventWithTags, IHerd, IPlan, ILayer, IProvider, 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,7 +21,8 @@ export declare class HerdModule {
21
21
  labels: string[];
22
22
  plans: IPlan[];
23
23
  layers: ILayer[];
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[]);
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[]);
25
26
  to_serializable(): IHerdModule;
26
27
  static from_herd(herd: IHerd, client: SupabaseClient): Promise<HerdModule>;
27
28
  }
@@ -39,6 +40,7 @@ export interface IHerdModule {
39
40
  zones: IZoneWithActions[];
40
41
  sessions: ISessionWithCoordinates[];
41
42
  layers: ILayer[];
43
+ providers: IProvider[];
42
44
  }
43
45
  export interface IHerdModulesResponse {
44
46
  data: IHerdModule[];
@@ -4,6 +4,7 @@ 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";
7
8
  import { server_get_events_and_tags_for_devices_batch } from "../helpers/tags";
8
9
  import { server_get_users_with_herd_access } from "../helpers/users";
9
10
  import { EnumWebResponse } from "./requests";
@@ -18,7 +19,7 @@ export var EnumHerdModulesLoadingState;
18
19
  EnumHerdModulesLoadingState["UNSUCCESSFULLY_LOADED"] = "UNSUCCESSFULLY_LOADED";
19
20
  })(EnumHerdModulesLoadingState || (EnumHerdModulesLoadingState = {}));
20
21
  export class HerdModule {
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 = []) {
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 = []) {
22
23
  this.user_roles = null;
23
24
  this.events_page_index = 0;
24
25
  this.total_events = 0;
@@ -26,6 +27,7 @@ export class HerdModule {
26
27
  this.labels = [];
27
28
  this.plans = [];
28
29
  this.layers = [];
30
+ this.providers = [];
29
31
  this.herd = herd;
30
32
  this.devices = devices;
31
33
  this.events = events;
@@ -39,6 +41,7 @@ export class HerdModule {
39
41
  this.zones = zones;
40
42
  this.sessions = sessions;
41
43
  this.layers = layers;
44
+ this.providers = providers;
42
45
  }
43
46
  to_serializable() {
44
47
  return {
@@ -55,6 +58,7 @@ export class HerdModule {
55
58
  zones: this.zones,
56
59
  sessions: this.sessions,
57
60
  layers: this.layers,
61
+ providers: this.providers,
58
62
  };
59
63
  }
60
64
  static async from_herd(herd, client) {
@@ -95,7 +99,7 @@ export class HerdModule {
95
99
  }
96
100
  }
97
101
  // Run all remaining requests in parallel with individual error handling
98
- const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers,] = await Promise.allSettled([
102
+ const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers, res_providers,] = await Promise.allSettled([
99
103
  server_get_more_zones_and_actions_for_herd(herd.id, 0, 10).catch((error) => {
100
104
  console.warn(`[HerdModule] Failed to get zones and actions:`, error);
101
105
  return { status: EnumWebResponse.ERROR, data: null };
@@ -114,12 +118,20 @@ export class HerdModule {
114
118
  }),
115
119
  server_get_sessions_by_herd_id(herd.id).catch((error) => {
116
120
  console.warn(`[HerdModule] Failed to get sessions:`, error);
117
- return { status: EnumWebResponse.ERROR, data: [], msg: error.message };
121
+ return {
122
+ status: EnumWebResponse.ERROR,
123
+ data: [],
124
+ msg: error.message,
125
+ };
118
126
  }),
119
127
  server_get_layers_by_herd(herd.id).catch((error) => {
120
128
  console.warn(`[HerdModule] Failed to get layers:`, error);
121
129
  return { status: EnumWebResponse.ERROR, data: null };
122
130
  }),
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
+ }),
123
135
  ]);
124
136
  // Assign recent events to devices from batch results
125
137
  for (let i = 0; i < new_devices.length; i++) {
@@ -148,23 +160,28 @@ export class HerdModule {
148
160
  const plans = res_plans.status === "fulfilled" && res_plans.value?.data
149
161
  ? res_plans.value.data
150
162
  : [];
151
- const sessions = res_sessions.status === "fulfilled" && res_sessions.value?.data ? res_sessions.value.data : [];
163
+ const sessions = res_sessions.status === "fulfilled" && res_sessions.value?.data
164
+ ? res_sessions.value.data
165
+ : [];
152
166
  const layers = res_layers.status === "fulfilled" && res_layers.value?.data
153
167
  ? res_layers.value.data
154
168
  : [];
169
+ const providers = res_providers.status === "fulfilled" && res_providers.value?.data
170
+ ? res_providers.value.data
171
+ : [];
155
172
  // TODO: store in DB and retrieve on load?
156
173
  const newLabels = LABELS;
157
174
  const endTime = Date.now();
158
175
  const loadTime = endTime - startTime;
159
176
  console.log(`[HerdModule] Loaded herd ${herd.slug} in ${loadTime}ms (${new_devices.length} devices)`);
160
- return new HerdModule(herd, new_devices, [], Date.now(), user_roles, 0, total_events, total_events, newLabels, plans, zones, sessions, layers);
177
+ return new HerdModule(herd, new_devices, [], Date.now(), user_roles, 0, total_events, total_events, newLabels, plans, zones, sessions, layers, providers);
161
178
  }
162
179
  catch (error) {
163
180
  const endTime = Date.now();
164
181
  const loadTime = endTime - startTime;
165
182
  console.error(`[HerdModule] Critical error in HerdModule.from_herd (${loadTime}ms):`, error);
166
183
  // Return a minimal but valid HerdModule instance to prevent complete failure
167
- return new HerdModule(herd, [], [], Date.now(), null, 0, 0, 0, [], [], [], [], []);
184
+ return new HerdModule(herd, [], [], Date.now(), null, 0, 0, 0, [], [], [], [], [], []);
168
185
  }
169
186
  }
170
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.85",
3
+ "version": "1.0.86",
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",