@adventurelabs/scout-core 2.0.18 → 2.1.0
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/client/index.d.ts +4 -0
- package/dist/client/index.js +4 -0
- package/dist/helpers/cache.d.ts +4 -4
- package/dist/helpers/cache.js +52 -17
- package/dist/helpers/eventMedia.d.ts +6 -4
- package/dist/helpers/eventMedia.js +7 -6
- package/dist/helpers/gallery.d.ts +4 -0
- package/dist/helpers/gallery.js +5 -0
- package/dist/helpers/gallery.queries.d.ts +14 -0
- package/dist/helpers/gallery.queries.js +49 -0
- package/dist/helpers/galleryMedia.d.ts +9 -0
- package/dist/helpers/galleryMedia.js +41 -0
- package/dist/helpers/herd_modules.queries.js +14 -6
- package/dist/helpers/mediaRows.d.ts +12 -0
- package/dist/helpers/mediaRows.js +80 -0
- package/dist/helpers/mediaState.d.ts +33 -0
- package/dist/helpers/mediaState.js +86 -0
- package/dist/helpers/media_urls.d.ts +2 -1
- package/dist/helpers/media_urls.js +13 -2
- package/dist/helpers/parts.d.ts +7 -2
- package/dist/helpers/parts.js +16 -7
- package/dist/helpers/parts_server.d.ts +1 -1
- package/dist/helpers/product_catalog.d.ts +9 -0
- package/dist/helpers/product_catalog.js +36 -0
- package/dist/helpers/session_incidents_server.d.ts +5 -0
- package/dist/helpers/storage_internal.d.ts +4 -3
- package/dist/helpers/storage_internal.js +46 -14
- package/dist/helpers/tags.queries.js +2 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useInfiniteQuery.d.ts +3 -1
- package/dist/hooks/useInfiniteQuery.js +37 -43
- package/dist/hooks/useScoutRealtimeBroadcast.d.ts +4 -0
- package/dist/hooks/useScoutRealtimeBroadcast.js +76 -11
- package/dist/hooks/useScoutRefresh.js +18 -11
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +4 -0
- package/dist/store/api.d.ts +389 -9
- package/dist/store/api.js +163 -75
- package/dist/store/configureStore.d.ts +4 -0
- package/dist/store/hooks.d.ts +4 -1
- package/dist/store/hooks.js +16 -0
- package/dist/store/scout.d.ts +4 -2
- package/dist/store/scout.js +21 -2
- package/dist/types/db.d.ts +19 -5
- package/dist/types/herd_module.d.ts +12 -1
- package/dist/types/supabase.d.ts +77 -0
- package/package.json +4 -2
package/dist/client/index.d.ts
CHANGED
|
@@ -5,9 +5,12 @@ export * from "../helpers/bounding_boxes";
|
|
|
5
5
|
export * from "../helpers/email";
|
|
6
6
|
export * from "../helpers/eventMedia";
|
|
7
7
|
export * from "../helpers/eventUtils";
|
|
8
|
+
export * from "../helpers/galleryMedia";
|
|
8
9
|
export { default as get_gps_center } from "../helpers/gps";
|
|
9
10
|
export * from "../helpers/herd_modules_equal";
|
|
10
11
|
export * from "../helpers/location";
|
|
12
|
+
export * from "../helpers/mediaState";
|
|
13
|
+
export * from "../helpers/product_catalog";
|
|
11
14
|
export * from "../helpers/realtime_topic_access";
|
|
12
15
|
export * from "../helpers/storagePath";
|
|
13
16
|
export * from "../helpers/time";
|
|
@@ -45,6 +48,7 @@ export { create_document_template_query as create_document_template, get_documen
|
|
|
45
48
|
export { search_embeddings_vertex_multimodal_001_query as search_embeddings_vertex_multimodal_001 } from "../helpers/embeddings.queries";
|
|
46
49
|
export { delete_event_query as delete_event, get_event_by_id_query as get_event_by_id, get_total_events_by_herd_query as get_total_events_by_herd, insert_event_query as insert_event, match_events_by_vertex_embedding_query as match_events_by_vertex_embedding, update_event_query as update_event, } from "../helpers/events.queries";
|
|
47
50
|
export { create_battery_cycle_count_query as create_battery_cycle_count, delete_battery_cycle_count_query as delete_battery_cycle_count, get_battery_cycle_counts_by_battery_id_query as get_battery_cycle_counts_by_battery_id, update_battery_cycle_count_query as update_battery_cycle_count, } from "../helpers/battery_cycle_counts.queries";
|
|
51
|
+
export { get_gallery_items_by_ids_query as get_gallery_items_by_ids, type IGalleryItemsByIdsArgs, } from "../helpers/gallery.queries";
|
|
48
52
|
export { get_health_metrics_query as get_health_metrics, get_health_metrics_summary_query as get_health_metrics_summary, } from "../helpers/health_metrics.queries";
|
|
49
53
|
export { get_herd_media_usage_query as get_herd_media_usage, get_latest_herd_media_usage_query as get_latest_herd_media_usage, } from "../helpers/herd_media_usage.queries";
|
|
50
54
|
export { check_device_online_status_query as check_device_online_status, get_heartbeats_by_device_query as get_heartbeats_by_device, get_last_heartbeat_by_device_query as get_last_heartbeat_by_device, } from "../helpers/heartbeats.queries";
|
package/dist/client/index.js
CHANGED
|
@@ -8,9 +8,12 @@ export * from "../helpers/bounding_boxes";
|
|
|
8
8
|
export * from "../helpers/email";
|
|
9
9
|
export * from "../helpers/eventMedia";
|
|
10
10
|
export * from "../helpers/eventUtils";
|
|
11
|
+
export * from "../helpers/galleryMedia";
|
|
11
12
|
export { default as get_gps_center } from "../helpers/gps";
|
|
12
13
|
export * from "../helpers/herd_modules_equal";
|
|
13
14
|
export * from "../helpers/location";
|
|
15
|
+
export * from "../helpers/mediaState";
|
|
16
|
+
export * from "../helpers/product_catalog";
|
|
14
17
|
export * from "../helpers/realtime_topic_access";
|
|
15
18
|
export * from "../helpers/storagePath";
|
|
16
19
|
export * from "../helpers/time";
|
|
@@ -50,6 +53,7 @@ export { create_document_template_query as create_document_template, get_documen
|
|
|
50
53
|
export { search_embeddings_vertex_multimodal_001_query as search_embeddings_vertex_multimodal_001 } from "../helpers/embeddings.queries";
|
|
51
54
|
export { delete_event_query as delete_event, get_event_by_id_query as get_event_by_id, get_total_events_by_herd_query as get_total_events_by_herd, insert_event_query as insert_event, match_events_by_vertex_embedding_query as match_events_by_vertex_embedding, update_event_query as update_event, } from "../helpers/events.queries";
|
|
52
55
|
export { create_battery_cycle_count_query as create_battery_cycle_count, delete_battery_cycle_count_query as delete_battery_cycle_count, get_battery_cycle_counts_by_battery_id_query as get_battery_cycle_counts_by_battery_id, update_battery_cycle_count_query as update_battery_cycle_count, } from "../helpers/battery_cycle_counts.queries";
|
|
56
|
+
export { get_gallery_items_by_ids_query as get_gallery_items_by_ids, } from "../helpers/gallery.queries";
|
|
53
57
|
export { get_health_metrics_query as get_health_metrics, get_health_metrics_summary_query as get_health_metrics_summary, } from "../helpers/health_metrics.queries";
|
|
54
58
|
export { get_herd_media_usage_query as get_herd_media_usage, get_latest_herd_media_usage_query as get_latest_herd_media_usage, } from "../helpers/herd_media_usage.queries";
|
|
55
59
|
export { check_device_online_status_query as check_device_online_status, get_heartbeats_by_device_query as get_heartbeats_by_device, get_last_heartbeat_by_device_query as get_last_heartbeat_by_device, } from "../helpers/heartbeats.queries";
|
package/dist/helpers/cache.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IHerdModulesSnapshot } from "../types/herd_module";
|
|
2
2
|
export type JwtMintCacheKey = "client_abilities" | "pubsub";
|
|
3
3
|
type CachedJwtMint = {
|
|
4
4
|
token: string;
|
|
@@ -53,8 +53,8 @@ export declare class ScoutCache {
|
|
|
53
53
|
private getDatabase;
|
|
54
54
|
private init;
|
|
55
55
|
private validateDatabaseSchema;
|
|
56
|
-
setHerdModules(
|
|
57
|
-
getHerdModules(userId?: string): Promise<CacheResult<
|
|
56
|
+
setHerdModules(snapshot: IHerdModulesSnapshot, ttlMs?: number, etag?: string, userId?: string): Promise<void>;
|
|
57
|
+
getHerdModules(userId?: string): Promise<CacheResult<IHerdModulesSnapshot>>;
|
|
58
58
|
setJwtMint<T extends CachedJwtMint>(key: JwtMintCacheKey, mint: T, userId?: string): Promise<void>;
|
|
59
59
|
getJwtMint<T extends CachedJwtMint>(key: JwtMintCacheKey, userId?: string): Promise<CacheResult<T>>;
|
|
60
60
|
clearJwtMint(key: JwtMintCacheKey, userId?: string): Promise<void>;
|
|
@@ -67,7 +67,7 @@ export declare class ScoutCache {
|
|
|
67
67
|
shouldRefresh: boolean;
|
|
68
68
|
reason: string;
|
|
69
69
|
}>;
|
|
70
|
-
preloadCache(loadFunction: () => Promise<
|
|
70
|
+
preloadCache(loadFunction: () => Promise<IHerdModulesSnapshot>, ttlMs?: number, userId?: string): Promise<void>;
|
|
71
71
|
getDefaultTtl(): number;
|
|
72
72
|
getCurrentDbVersion(): number;
|
|
73
73
|
isCacheVersionCompatible(userId?: string): Promise<boolean>;
|
package/dist/helpers/cache.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { EMPTY_PRODUCT_CATALOG } from "./product_catalog";
|
|
1
2
|
const LEGACY_DB_NAME = "ScoutCache";
|
|
2
|
-
const DB_VERSION =
|
|
3
|
+
const DB_VERSION = 10;
|
|
3
4
|
const HERD_MODULES_STORE = "herd_modules";
|
|
5
|
+
const PRODUCT_CATALOG_STORE = "product_catalog";
|
|
4
6
|
const JWT_MINTS_STORE = "jwt_mints";
|
|
5
7
|
const CACHE_METADATA_STORE = "cache_metadata";
|
|
8
|
+
const PRODUCT_CATALOG_KEY = "product_catalog";
|
|
6
9
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
10
|
let legacyDbDeleteStarted = false;
|
|
8
11
|
function getSupabaseProjectRef() {
|
|
@@ -118,6 +121,9 @@ export class ScoutCache {
|
|
|
118
121
|
herdModulesStore.createIndex("dbVersion", "dbVersion", {
|
|
119
122
|
unique: false,
|
|
120
123
|
});
|
|
124
|
+
db.createObjectStore(PRODUCT_CATALOG_STORE, {
|
|
125
|
+
keyPath: "key",
|
|
126
|
+
});
|
|
121
127
|
const jwtMintsStore = db.createObjectStore(JWT_MINTS_STORE, {
|
|
122
128
|
keyPath: "key",
|
|
123
129
|
});
|
|
@@ -149,18 +155,20 @@ export class ScoutCache {
|
|
|
149
155
|
}
|
|
150
156
|
validateDatabaseSchema(db) {
|
|
151
157
|
return (db.objectStoreNames.contains(HERD_MODULES_STORE) &&
|
|
158
|
+
db.objectStoreNames.contains(PRODUCT_CATALOG_STORE) &&
|
|
152
159
|
db.objectStoreNames.contains(JWT_MINTS_STORE) &&
|
|
153
160
|
db.objectStoreNames.contains(CACHE_METADATA_STORE));
|
|
154
161
|
}
|
|
155
|
-
async setHerdModules(
|
|
162
|
+
async setHerdModules(snapshot, ttlMs = DEFAULT_TTL_MS, etag, userId) {
|
|
156
163
|
const db = await this.getDatabase(userId);
|
|
157
|
-
const transaction = db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readwrite");
|
|
164
|
+
const transaction = db.transaction([HERD_MODULES_STORE, PRODUCT_CATALOG_STORE, CACHE_METADATA_STORE], "readwrite");
|
|
158
165
|
const completion = transactionComplete(transaction);
|
|
159
166
|
const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
|
|
167
|
+
const productCatalogStore = transaction.objectStore(PRODUCT_CATALOG_STORE);
|
|
160
168
|
const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
|
|
161
169
|
const timestamp = Date.now();
|
|
162
170
|
herdModulesStore.clear();
|
|
163
|
-
|
|
171
|
+
snapshot.herd_modules.forEach((herdModule) => {
|
|
164
172
|
if (herdModule.herd.id == null)
|
|
165
173
|
return;
|
|
166
174
|
herdModulesStore.put({
|
|
@@ -170,6 +178,12 @@ export class ScoutCache {
|
|
|
170
178
|
dbVersion: DB_VERSION,
|
|
171
179
|
});
|
|
172
180
|
});
|
|
181
|
+
productCatalogStore.put({
|
|
182
|
+
key: PRODUCT_CATALOG_KEY,
|
|
183
|
+
data: snapshot.product_catalog,
|
|
184
|
+
timestamp,
|
|
185
|
+
dbVersion: DB_VERSION,
|
|
186
|
+
});
|
|
173
187
|
const metadata = {
|
|
174
188
|
key: "herd_modules",
|
|
175
189
|
timestamp,
|
|
@@ -185,10 +199,11 @@ export class ScoutCache {
|
|
|
185
199
|
async getHerdModules(userId) {
|
|
186
200
|
const scopedUserId = userId ?? this.userId ?? undefined;
|
|
187
201
|
const db = await this.getDatabase(scopedUserId);
|
|
188
|
-
const transaction = db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readonly");
|
|
202
|
+
const transaction = db.transaction([HERD_MODULES_STORE, PRODUCT_CATALOG_STORE, CACHE_METADATA_STORE], "readonly");
|
|
189
203
|
return new Promise((resolve, reject) => {
|
|
190
204
|
transaction.onerror = () => reject(transaction.error);
|
|
191
205
|
const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
|
|
206
|
+
const productCatalogStore = transaction.objectStore(PRODUCT_CATALOG_STORE);
|
|
192
207
|
const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
|
|
193
208
|
const metadataRequest = metadataStore.get("herd_modules");
|
|
194
209
|
metadataRequest.onsuccess = () => {
|
|
@@ -210,10 +225,25 @@ export class ScoutCache {
|
|
|
210
225
|
}
|
|
211
226
|
const age = now - metadata.timestamp;
|
|
212
227
|
const isStale = age > metadata.ttl;
|
|
228
|
+
let herdModules = null;
|
|
229
|
+
let productCatalog = null;
|
|
230
|
+
const resolveWhenLoaded = () => {
|
|
231
|
+
if (herdModules === null || productCatalog === null)
|
|
232
|
+
return;
|
|
233
|
+
resolve({
|
|
234
|
+
data: {
|
|
235
|
+
herd_modules: herdModules,
|
|
236
|
+
product_catalog: productCatalog,
|
|
237
|
+
},
|
|
238
|
+
isStale,
|
|
239
|
+
age,
|
|
240
|
+
metadata,
|
|
241
|
+
});
|
|
242
|
+
};
|
|
213
243
|
const getAllRequest = herdModulesStore.getAll();
|
|
214
244
|
getAllRequest.onsuccess = () => {
|
|
215
245
|
const cacheEntries = getAllRequest.result;
|
|
216
|
-
|
|
246
|
+
herdModules = cacheEntries
|
|
217
247
|
.filter((entry) => entry.data &&
|
|
218
248
|
entry.data.herd &&
|
|
219
249
|
entry.data.herd.id != null &&
|
|
@@ -226,12 +256,16 @@ export class ScoutCache {
|
|
|
226
256
|
else {
|
|
227
257
|
this.stats.misses++;
|
|
228
258
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
259
|
+
resolveWhenLoaded();
|
|
260
|
+
};
|
|
261
|
+
const catalogRequest = productCatalogStore.get(PRODUCT_CATALOG_KEY);
|
|
262
|
+
catalogRequest.onsuccess = () => {
|
|
263
|
+
const entry = catalogRequest.result;
|
|
264
|
+
productCatalog =
|
|
265
|
+
entry?.dbVersion === DB_VERSION && entry.data
|
|
266
|
+
? entry.data
|
|
267
|
+
: EMPTY_PRODUCT_CATALOG;
|
|
268
|
+
resolveWhenLoaded();
|
|
235
269
|
};
|
|
236
270
|
};
|
|
237
271
|
});
|
|
@@ -295,9 +329,10 @@ export class ScoutCache {
|
|
|
295
329
|
if (!userId && !this.dbName)
|
|
296
330
|
return;
|
|
297
331
|
const db = await this.getDatabase(userId);
|
|
298
|
-
const transaction = db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readwrite");
|
|
332
|
+
const transaction = db.transaction([HERD_MODULES_STORE, PRODUCT_CATALOG_STORE, CACHE_METADATA_STORE], "readwrite");
|
|
299
333
|
const completion = transactionComplete(transaction);
|
|
300
334
|
transaction.objectStore(HERD_MODULES_STORE).clear();
|
|
335
|
+
transaction.objectStore(PRODUCT_CATALOG_STORE).clear();
|
|
301
336
|
transaction.objectStore(CACHE_METADATA_STORE).delete("herd_modules");
|
|
302
337
|
await completion;
|
|
303
338
|
}
|
|
@@ -308,7 +343,7 @@ export class ScoutCache {
|
|
|
308
343
|
const result = await this.getHerdModules(userId);
|
|
309
344
|
const totalRequests = this.stats.hits + this.stats.misses;
|
|
310
345
|
const hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
|
|
311
|
-
const size = result.data?.length || 0;
|
|
346
|
+
const size = result.data?.herd_modules.length || 0;
|
|
312
347
|
return {
|
|
313
348
|
size,
|
|
314
349
|
lastUpdated: result.data ? Date.now() - result.age : 0,
|
|
@@ -334,7 +369,7 @@ export class ScoutCache {
|
|
|
334
369
|
return { shouldRefresh: true, reason: "Force refresh requested" };
|
|
335
370
|
}
|
|
336
371
|
const result = await this.getHerdModules(userId);
|
|
337
|
-
if (!result.data || result.data.length === 0) {
|
|
372
|
+
if (!result.data || result.data.herd_modules.length === 0) {
|
|
338
373
|
return { shouldRefresh: true, reason: "No cached data" };
|
|
339
374
|
}
|
|
340
375
|
if (!result.metadata ||
|
|
@@ -360,8 +395,8 @@ export class ScoutCache {
|
|
|
360
395
|
try {
|
|
361
396
|
console.log("[ScoutCache] Starting background cache preload...");
|
|
362
397
|
const startTime = Date.now();
|
|
363
|
-
const
|
|
364
|
-
await this.setHerdModules(
|
|
398
|
+
const snapshot = await loadFunction();
|
|
399
|
+
await this.setHerdModules(snapshot, ttlMs, undefined, userId);
|
|
365
400
|
const duration = Date.now() - startTime;
|
|
366
401
|
console.log(`[ScoutCache] Background preload completed in ${duration}ms`);
|
|
367
402
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { ISignedMediaUrls } from "../types/db";
|
|
1
|
+
import type { ISignedMediaUrls } from "../types/db";
|
|
2
2
|
/** Events arrive as table rows and as the event_and_tags composite, so match on shape. */
|
|
3
|
-
export type
|
|
3
|
+
export type IMediaPaths = {
|
|
4
4
|
file_path?: string | null;
|
|
5
5
|
thumbnail_file_path?: string | null;
|
|
6
6
|
proxy_file_path?: string | null;
|
|
7
7
|
media_url?: string | null;
|
|
8
8
|
};
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export type IEventMediaPaths = IMediaPaths;
|
|
10
|
+
/** Collect unique non-empty original and derivative storage paths. */
|
|
11
|
+
export declare function collectMediaStoragePaths(rows: Iterable<IMediaPaths | null | undefined>): string[];
|
|
12
|
+
export declare const collectEventStoragePaths: typeof collectMediaStoragePaths;
|
|
11
13
|
/** Attach signed URLs for original, thumbnail, and proxy storage paths. */
|
|
12
14
|
export declare function withEventMediaUrls<T extends IEventMediaPaths>(event: T, urlMap: Map<string, string | null | undefined>): T & ISignedMediaUrls;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isNonEmptyStorageFilePath, signedUrlForPath } from "./storagePath";
|
|
2
|
-
/** Collect unique non-empty
|
|
3
|
-
export function
|
|
2
|
+
/** Collect unique non-empty original and derivative storage paths. */
|
|
3
|
+
export function collectMediaStoragePaths(rows) {
|
|
4
4
|
const seen = new Set();
|
|
5
5
|
const paths = [];
|
|
6
|
-
for (const
|
|
6
|
+
for (const row of rows) {
|
|
7
7
|
for (const path of [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
row?.file_path,
|
|
9
|
+
row?.thumbnail_file_path,
|
|
10
|
+
row?.proxy_file_path,
|
|
11
11
|
]) {
|
|
12
12
|
if (isNonEmptyStorageFilePath(path) && !seen.has(path)) {
|
|
13
13
|
seen.add(path);
|
|
@@ -17,6 +17,7 @@ export function collectEventStoragePaths(events) {
|
|
|
17
17
|
}
|
|
18
18
|
return paths;
|
|
19
19
|
}
|
|
20
|
+
export const collectEventStoragePaths = collectMediaStoragePaths;
|
|
20
21
|
/** Attach signed URLs for original, thumbnail, and proxy storage paths. */
|
|
21
22
|
export function withEventMediaUrls(event, urlMap) {
|
|
22
23
|
return {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IGalleryItem } from "../types/db";
|
|
2
|
+
import type { IWebResponseCompatible } from "../types/requests";
|
|
3
|
+
import { type IGalleryItemsByIdsArgs } from "./gallery.queries";
|
|
4
|
+
export declare function server_get_gallery_items_by_ids(args?: IGalleryItemsByIdsArgs): Promise<IWebResponseCompatible<IGalleryItem[]>>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { IGalleryItem } from "../types/db";
|
|
3
|
+
import { type IWebResponseCompatible } from "../types/requests";
|
|
4
|
+
import type { Database } from "../types/supabase";
|
|
5
|
+
import { type GalleryItemKey } from "./galleryMedia";
|
|
6
|
+
/** Missing, inactive, log-only, and pathless rows are omitted from the successful result. */
|
|
7
|
+
export interface IGalleryItemsByIdsArgs {
|
|
8
|
+
eventIds?: readonly number[];
|
|
9
|
+
artifactIds?: readonly number[];
|
|
10
|
+
/** Optional mixed event/artifact confidence order; its ids are fetched automatically. */
|
|
11
|
+
rankedIds?: readonly GalleryItemKey[];
|
|
12
|
+
}
|
|
13
|
+
/** Hydrates ids into signed gallery items in one read, ordered as asked for. */
|
|
14
|
+
export declare function get_gallery_items_by_ids_query(client: SupabaseClient<Database>, args?: IGalleryItemsByIdsArgs): Promise<IWebResponseCompatible<IGalleryItem[]>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { IWebResponse } from "../types/requests";
|
|
2
|
+
import { rankGalleryItems } from "./galleryMedia";
|
|
3
|
+
import { addGalleryMediaUrls } from "./media_urls";
|
|
4
|
+
const normalizedIds = (ids = []) => ids.every((id) => Number.isSafeInteger(id) && id > 0)
|
|
5
|
+
? [...new Set(ids)]
|
|
6
|
+
: null;
|
|
7
|
+
const normalizedRanking = (items = []) => {
|
|
8
|
+
const unique = new Map();
|
|
9
|
+
for (const item of items) {
|
|
10
|
+
if ((item.feedType !== "event" && item.feedType !== "artifact") ||
|
|
11
|
+
!Number.isSafeInteger(item.id) ||
|
|
12
|
+
item.id <= 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const key = `${item.feedType}:${item.id}`;
|
|
16
|
+
if (!unique.has(key))
|
|
17
|
+
unique.set(key, item);
|
|
18
|
+
}
|
|
19
|
+
return [...unique.values()];
|
|
20
|
+
};
|
|
21
|
+
/** Hydrates ids into signed gallery items in one read, ordered as asked for. */
|
|
22
|
+
export async function get_gallery_items_by_ids_query(client, args = {}) {
|
|
23
|
+
const rankedIds = normalizedRanking(args.rankedIds);
|
|
24
|
+
const rankedEventIds = [];
|
|
25
|
+
const rankedArtifactIds = [];
|
|
26
|
+
rankedIds?.forEach(({ feedType, id }) => {
|
|
27
|
+
(feedType === "event" ? rankedEventIds : rankedArtifactIds).push(id);
|
|
28
|
+
});
|
|
29
|
+
const eventIds = normalizedIds([...(args.eventIds ?? []), ...rankedEventIds]);
|
|
30
|
+
const artifactIds = normalizedIds([
|
|
31
|
+
...(args.artifactIds ?? []),
|
|
32
|
+
...rankedArtifactIds,
|
|
33
|
+
]);
|
|
34
|
+
if (!rankedIds || !eventIds || !artifactIds) {
|
|
35
|
+
return IWebResponse.error("Gallery item IDs must be positive safe integers with valid feed types").to_compatible();
|
|
36
|
+
}
|
|
37
|
+
if (eventIds.length === 0 && artifactIds.length === 0) {
|
|
38
|
+
return IWebResponse.success([]).to_compatible();
|
|
39
|
+
}
|
|
40
|
+
const { data, error } = await client.rpc("get_gallery_items_by_ids", {
|
|
41
|
+
event_ids: eventIds,
|
|
42
|
+
artifact_ids: artifactIds,
|
|
43
|
+
});
|
|
44
|
+
if (error) {
|
|
45
|
+
return IWebResponse.error(error.message).to_compatible();
|
|
46
|
+
}
|
|
47
|
+
const items = await addGalleryMediaUrls(client, data ?? []);
|
|
48
|
+
return IWebResponse.success(rankGalleryItems(items, eventIds, artifactIds, rankedIds)).to_compatible();
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IGalleryItem, IGalleryItemRow } from "../types/db";
|
|
2
|
+
export interface GalleryItemKey {
|
|
3
|
+
feedType: "event" | "artifact";
|
|
4
|
+
id: number;
|
|
5
|
+
}
|
|
6
|
+
/** Signs media the way events do, then adds the derived kind and readiness a tile renders. */
|
|
7
|
+
export declare function withGalleryMediaUrls(item: IGalleryItemRow, urlMap: Map<string, string | null | undefined>): IGalleryItem;
|
|
8
|
+
/** Uses a mixed ranking when supplied, otherwise each type's list position and then recency. */
|
|
9
|
+
export declare function rankGalleryItems<T extends IGalleryItemRow>(items: T[], eventIds: readonly number[], artifactIds: readonly number[], rankedIds?: readonly GalleryItemKey[]): T[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { withEventMediaUrls } from "./eventMedia";
|
|
2
|
+
import { classifyMedia, mediaStatus } from "./mediaState";
|
|
3
|
+
/** Signs media the way events do, then adds the derived kind and readiness a tile renders. */
|
|
4
|
+
export function withGalleryMediaUrls(item, urlMap) {
|
|
5
|
+
const withUrls = withEventMediaUrls(item, urlMap);
|
|
6
|
+
return {
|
|
7
|
+
...withUrls,
|
|
8
|
+
media_kind: classifyMedia(withUrls),
|
|
9
|
+
media_status: mediaStatus(withUrls),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Uses a mixed ranking when supplied, otherwise each type's list position and then recency. */
|
|
13
|
+
export function rankGalleryItems(items, eventIds, artifactIds, rankedIds) {
|
|
14
|
+
const mixedRanks = new Map();
|
|
15
|
+
rankedIds?.forEach(({ feedType, id }, index) => {
|
|
16
|
+
const key = `${feedType}:${id}`;
|
|
17
|
+
if (!mixedRanks.has(key))
|
|
18
|
+
mixedRanks.set(key, index);
|
|
19
|
+
});
|
|
20
|
+
const rankOf = (ids) => {
|
|
21
|
+
const ranks = new Map();
|
|
22
|
+
ids.forEach((id, index) => {
|
|
23
|
+
if (!ranks.has(id))
|
|
24
|
+
ranks.set(id, index);
|
|
25
|
+
});
|
|
26
|
+
return ranks;
|
|
27
|
+
};
|
|
28
|
+
const eventRanks = rankOf(eventIds);
|
|
29
|
+
const artifactRanks = rankOf(artifactIds);
|
|
30
|
+
const rank = (item) => {
|
|
31
|
+
if (mixedRanks.size > 0) {
|
|
32
|
+
return (mixedRanks.get(`${item.feed_type}:${item.id}`) ??
|
|
33
|
+
Number.MAX_SAFE_INTEGER);
|
|
34
|
+
}
|
|
35
|
+
const ranks = item.feed_type === "artifact" ? artifactRanks : eventRanks;
|
|
36
|
+
const position = item.id == null ? undefined : ranks.get(item.id);
|
|
37
|
+
return position ?? Number.MAX_SAFE_INTEGER;
|
|
38
|
+
};
|
|
39
|
+
return [...items].sort((left, right) => rank(left) - rank(right) ||
|
|
40
|
+
(right.sort_ts ?? "").localeCompare(left.sort_ts ?? ""));
|
|
41
|
+
}
|
|
@@ -5,13 +5,14 @@ import { get_devices_by_herd_query } from "./devices.queries";
|
|
|
5
5
|
import { get_herds_with_location_query } from "./herds.queries";
|
|
6
6
|
import { get_layers_by_herd_ids_query, get_plans_by_herd_ids_query, get_providers_by_herd_ids_query, } from "./herd_scoped.queries";
|
|
7
7
|
import { get_parts_by_herd_ids } from "./parts";
|
|
8
|
+
import { EMPTY_PRODUCT_CATALOG } from "./product_catalog";
|
|
8
9
|
import { get_session_summaries_query } from "./session_summaries.queries";
|
|
9
10
|
import { get_session_usage_over_time_by_herd_query } from "./sessions.queries";
|
|
10
11
|
import { get_users_with_herd_access_batch_query } from "./users.queries";
|
|
11
12
|
import { get_more_zones_and_actions_for_herd_query } from "./zones.queries";
|
|
12
|
-
async function hashHerdModules(modules) {
|
|
13
|
+
async function hashHerdModules(modules, productCatalog) {
|
|
13
14
|
const stable = modules.map(({ timestamp_last_refreshed: _ts, ...rest }) => rest);
|
|
14
|
-
const bytes = new TextEncoder().encode(JSON.stringify(stable));
|
|
15
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ modules: stable, product_catalog: productCatalog }));
|
|
15
16
|
const digest = await crypto.subtle.digest("SHA-1", bytes);
|
|
16
17
|
return Array.from(new Uint8Array(digest))
|
|
17
18
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
@@ -25,6 +26,7 @@ export async function load_herd_modules_query(client) {
|
|
|
25
26
|
status: EnumWebResponse.ERROR,
|
|
26
27
|
msg: herdsResult.msg ?? "Failed to load herds",
|
|
27
28
|
data: null,
|
|
29
|
+
product_catalog: EMPTY_PRODUCT_CATALOG,
|
|
28
30
|
time_finished: Date.now(),
|
|
29
31
|
time_sent: Date.now(),
|
|
30
32
|
server_processing_time_ms: Date.now() - startTime,
|
|
@@ -39,10 +41,11 @@ export async function load_herd_modules_query(client) {
|
|
|
39
41
|
status: EnumWebResponse.SUCCESS,
|
|
40
42
|
msg: "No herds accessible",
|
|
41
43
|
data: [],
|
|
44
|
+
product_catalog: EMPTY_PRODUCT_CATALOG,
|
|
42
45
|
time_finished: endTime,
|
|
43
46
|
time_sent: endTime,
|
|
44
47
|
server_processing_time_ms: endTime - startTime,
|
|
45
|
-
content_hash: await hashHerdModules([]),
|
|
48
|
+
content_hash: await hashHerdModules([], EMPTY_PRODUCT_CATALOG),
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
const herdIds = herds.map((herd) => herd.id);
|
|
@@ -53,19 +56,23 @@ export async function load_herd_modules_query(client) {
|
|
|
53
56
|
}))));
|
|
54
57
|
const grouped = (query) => query.then((response) => response.data ?? {}).catch(() => ({}));
|
|
55
58
|
const unwrap = (response) => response.status === EnumWebResponse.SUCCESS ? response.data : null;
|
|
56
|
-
const [plansByHerd, layersByHerd, providersByHerd, usersByHerd,
|
|
59
|
+
const [plansByHerd, layersByHerd, providersByHerd, usersByHerd, partsWithCatalog, devicesResults, zonesResults, summariesResults, usageResults,] = await Promise.all([
|
|
57
60
|
grouped(get_plans_by_herd_ids_query(client, herdIds)),
|
|
58
61
|
grouped(get_layers_by_herd_ids_query(client, herdIds)),
|
|
59
62
|
grouped(get_providers_by_herd_ids_query(client, herdIds)),
|
|
60
63
|
get_users_with_herd_access_batch_query(client, herdIds)
|
|
61
64
|
.then(unwrap)
|
|
62
65
|
.catch(() => null),
|
|
63
|
-
|
|
66
|
+
get_parts_by_herd_ids(client, herdIds)
|
|
67
|
+
.then(unwrap)
|
|
68
|
+
.catch(() => null),
|
|
64
69
|
perHerd((id) => get_devices_by_herd_query(client, id)),
|
|
65
70
|
perHerd((id) => get_more_zones_and_actions_for_herd_query(client, id, 0, 10)),
|
|
66
71
|
perHerd((id) => get_session_summaries_query(client, { herd_id: id })),
|
|
67
72
|
perHerd((id) => get_session_usage_over_time_by_herd_query(client, id)),
|
|
68
73
|
]);
|
|
74
|
+
const partsByDevice = partsWithCatalog?.parts_by_device ?? {};
|
|
75
|
+
const productCatalog = partsWithCatalog?.product_catalog ?? EMPTY_PRODUCT_CATALOG;
|
|
69
76
|
const devicesByHerdIndex = devicesResults.map((response) => unwrap(response) ?? []);
|
|
70
77
|
const allDevices = devicesByHerdIndex.flat();
|
|
71
78
|
let apiKeysByDevice = {};
|
|
@@ -101,9 +108,10 @@ export async function load_herd_modules_query(client) {
|
|
|
101
108
|
status: EnumWebResponse.SUCCESS,
|
|
102
109
|
msg: "Herd modules loaded successfully",
|
|
103
110
|
data: serialized_herd_modules,
|
|
111
|
+
product_catalog: productCatalog,
|
|
104
112
|
time_finished: endTime,
|
|
105
113
|
time_sent: endTime,
|
|
106
114
|
server_processing_time_ms: endTime - startTime,
|
|
107
|
-
content_hash: await hashHerdModules(serialized_herd_modules),
|
|
115
|
+
content_hash: await hashHerdModules(serialized_herd_modules, productCatalog),
|
|
108
116
|
};
|
|
109
117
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const normalizePageLimit: (limit: number | undefined) => number;
|
|
2
|
+
/** Splits a page fetched with `limit + 1` rows; the extra row is the only proof of a next page. */
|
|
3
|
+
export declare function splitOverFetchedPage<TRow>(rows: TRow[], limit: number): {
|
|
4
|
+
rows: TRow[];
|
|
5
|
+
hasMore: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Fields re-minted on every fetch, so they never indicate a changed row. */
|
|
8
|
+
export declare const SIGNED_URL_FIELDS: readonly ["media_url", "thumbnail_url", "proxy_url"];
|
|
9
|
+
/** Structural equality ignoring re-minted URLs and the readiness read off them. */
|
|
10
|
+
export declare function rowsEqualIgnoringSignedUrls(left: unknown, right: unknown): boolean;
|
|
11
|
+
/** Reuses `previous` — keeping identity for memoized consumers — with the URLs from `next`. */
|
|
12
|
+
export declare function withRefreshedSignedUrls<TRow>(previous: TRow, next: TRow): TRow;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { dequal } from "dequal";
|
|
2
|
+
import { mediaStatus } from "./mediaState";
|
|
3
|
+
export const normalizePageLimit = (limit) => {
|
|
4
|
+
if (limit !== undefined && Number.isSafeInteger(limit) && limit > 0) {
|
|
5
|
+
return limit;
|
|
6
|
+
}
|
|
7
|
+
return 20;
|
|
8
|
+
};
|
|
9
|
+
/** Splits a page fetched with `limit + 1` rows; the extra row is the only proof of a next page. */
|
|
10
|
+
export function splitOverFetchedPage(rows, limit) {
|
|
11
|
+
const hasMore = rows.length > limit;
|
|
12
|
+
return { rows: hasMore ? rows.slice(0, limit) : rows, hasMore };
|
|
13
|
+
}
|
|
14
|
+
/** Fields re-minted on every fetch, so they never indicate a changed row. */
|
|
15
|
+
export const SIGNED_URL_FIELDS = [
|
|
16
|
+
"media_url",
|
|
17
|
+
"thumbnail_url",
|
|
18
|
+
"proxy_url",
|
|
19
|
+
];
|
|
20
|
+
/** Feed rows nest their media under these keys. */
|
|
21
|
+
const NESTED_MEDIA_FIELDS = ["event_data", "artifact_data"];
|
|
22
|
+
/** Read off the signed URLs, so it is recomputed with them rather than compared. */
|
|
23
|
+
const DERIVED_MEDIA_FIELD = "media_status";
|
|
24
|
+
const REMINTED_FIELD_SET = new Set([
|
|
25
|
+
...SIGNED_URL_FIELDS,
|
|
26
|
+
DERIVED_MEDIA_FIELD,
|
|
27
|
+
]);
|
|
28
|
+
const NESTED_MEDIA_FIELD_SET = new Set(NESTED_MEDIA_FIELDS);
|
|
29
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
const comparedKeys = (row) => Object.keys(row).filter((key) => !REMINTED_FIELD_SET.has(key));
|
|
31
|
+
/** Structural equality ignoring re-minted URLs and the readiness read off them. */
|
|
32
|
+
export function rowsEqualIgnoringSignedUrls(left, right) {
|
|
33
|
+
if (left === right)
|
|
34
|
+
return true;
|
|
35
|
+
if (!isRecord(left) || !isRecord(right))
|
|
36
|
+
return dequal(left, right);
|
|
37
|
+
const keys = comparedKeys(left);
|
|
38
|
+
if (keys.length !== comparedKeys(right).length)
|
|
39
|
+
return false;
|
|
40
|
+
// Only the nested media rows hold re-minted fields of their own; the rest compares whole.
|
|
41
|
+
return keys.every((key) => NESTED_MEDIA_FIELD_SET.has(key)
|
|
42
|
+
? rowsEqualIgnoringSignedUrls(left[key], right[key])
|
|
43
|
+
: dequal(left[key], right[key]));
|
|
44
|
+
}
|
|
45
|
+
/** A failed re-mint reports null, which never displaces a link that still works. */
|
|
46
|
+
const refreshedUrl = (previous, next) => next ?? previous;
|
|
47
|
+
function signedUrlsDiffer(previous, next) {
|
|
48
|
+
return (SIGNED_URL_FIELDS.some((field) => refreshedUrl(previous[field], next[field]) !== previous[field]) ||
|
|
49
|
+
NESTED_MEDIA_FIELDS.some((field) => {
|
|
50
|
+
const previousNested = previous[field];
|
|
51
|
+
const nextNested = next[field];
|
|
52
|
+
return (isRecord(previousNested) &&
|
|
53
|
+
isRecord(nextNested) &&
|
|
54
|
+
signedUrlsDiffer(previousNested, nextNested));
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/** Reuses `previous` — keeping identity for memoized consumers — with the URLs from `next`. */
|
|
58
|
+
export function withRefreshedSignedUrls(previous, next) {
|
|
59
|
+
if (!isRecord(previous) || !isRecord(next))
|
|
60
|
+
return previous;
|
|
61
|
+
if (!signedUrlsDiffer(previous, next))
|
|
62
|
+
return previous;
|
|
63
|
+
const merged = { ...previous };
|
|
64
|
+
for (const field of SIGNED_URL_FIELDS) {
|
|
65
|
+
if (field in previous || field in next) {
|
|
66
|
+
merged[field] = refreshedUrl(previous[field], next[field]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const field of NESTED_MEDIA_FIELDS) {
|
|
70
|
+
const previousNested = previous[field];
|
|
71
|
+
const nextNested = next[field];
|
|
72
|
+
if (isRecord(previousNested) && isRecord(nextNested)) {
|
|
73
|
+
merged[field] = withRefreshedSignedUrls(previousNested, nextNested);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (DERIVED_MEDIA_FIELD in merged) {
|
|
77
|
+
merged[DERIVED_MEDIA_FIELD] = mediaStatus(merged);
|
|
78
|
+
}
|
|
79
|
+
return merged;
|
|
80
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MediaKind, MediaStatus, MediaType } from "../types/db";
|
|
2
|
+
/** Extensions classified by the client; image/video lists mirror the queue predicates. */
|
|
3
|
+
export declare const VIDEO_FILE_EXTENSIONS: readonly ["mp4", "m4v", "webm", "mov", "mkv", "avi"];
|
|
4
|
+
export declare const IMAGE_FILE_EXTENSIONS: readonly ["jpg", "jpeg", "png", "gif", "webp", "bmp"];
|
|
5
|
+
export declare const AUDIO_FILE_EXTENSIONS: readonly ["wav", "mp3", "m4a", "aac", "flac", "ogg"];
|
|
6
|
+
/** Lowercased extension of a storage path or URL, or null when there is none. */
|
|
7
|
+
export declare function mediaFileExtension(filePath: string | null | undefined): string | null;
|
|
8
|
+
export interface MediaClassificationInput {
|
|
9
|
+
/** Storage path, or a legacy absolute media URL. */
|
|
10
|
+
file_path?: string | null;
|
|
11
|
+
media_url?: string | null;
|
|
12
|
+
/** Events carry the media_type enum. */
|
|
13
|
+
media_type?: MediaType | null;
|
|
14
|
+
/** Artifacts carry a modality, which is `video` for video captures and `sys_logs` etc. for logs. */
|
|
15
|
+
modality?: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** One classifier for events and artifacts; a recognized extension wins over the stored type. */
|
|
18
|
+
export declare function classifyMedia(row: MediaClassificationInput): MediaKind;
|
|
19
|
+
export declare const isVideoMedia: (row: MediaClassificationInput) => boolean;
|
|
20
|
+
export interface MediaStatusInput extends MediaClassificationInput {
|
|
21
|
+
thumbnail_file_path?: string | null;
|
|
22
|
+
thumbnail_generated_at?: string | null;
|
|
23
|
+
proxy_file_path?: string | null;
|
|
24
|
+
proxy_generated_at?: string | null;
|
|
25
|
+
processing_blocked_at?: string | null;
|
|
26
|
+
/** Signed URLs, as attached by the media helpers. */
|
|
27
|
+
thumbnail_url?: string | null;
|
|
28
|
+
proxy_url?: string | null;
|
|
29
|
+
}
|
|
30
|
+
/** Readiness of image/video derivatives or the original for media without derivatives. */
|
|
31
|
+
export declare function mediaStatus(row: MediaStatusInput): MediaStatus;
|
|
32
|
+
/** Readiness of the original media itself, ignoring derivatives. */
|
|
33
|
+
export declare function originalMediaStatus(row: Pick<MediaStatusInput, "file_path" | "media_url" | "processing_blocked_at">): MediaStatus;
|