@adventurelabs/scout-core 2.0.15 → 2.0.17

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.
@@ -3,6 +3,7 @@ export * from "../types";
3
3
  export * from "../helpers/artifactMedia";
4
4
  export * from "../helpers/bounding_boxes";
5
5
  export * from "../helpers/email";
6
+ export * from "../helpers/eventMedia";
6
7
  export * from "../helpers/eventUtils";
7
8
  export { default as get_gps_center } from "../helpers/gps";
8
9
  export * from "../helpers/herd_modules_equal";
@@ -6,6 +6,7 @@ export * from "../types";
6
6
  export * from "../helpers/artifactMedia";
7
7
  export * from "../helpers/bounding_boxes";
8
8
  export * from "../helpers/email";
9
+ export * from "../helpers/eventMedia";
9
10
  export * from "../helpers/eventUtils";
10
11
  export { default as get_gps_center } from "../helpers/gps";
11
12
  export * from "../helpers/herd_modules_equal";
@@ -1,4 +1,4 @@
1
- import { isNonEmptyStorageFilePath } from "./storagePath";
1
+ import { isNonEmptyStorageFilePath, signedUrlForPath } from "./storagePath";
2
2
  /** Collect unique non-empty storage paths used for artifact media signing. */
3
3
  export function collectArtifactStoragePaths(artifacts) {
4
4
  const seen = new Set();
@@ -17,9 +17,6 @@ export function collectArtifactStoragePaths(artifacts) {
17
17
  }
18
18
  return paths;
19
19
  }
20
- function signedUrlForPath(path, urlMap) {
21
- return isNonEmptyStorageFilePath(path) ? urlMap.get(path) ?? null : null;
22
- }
23
20
  /** Attach signed URLs for original, thumbnail, and proxy storage paths. */
24
21
  export function withArtifactMediaUrls(artifact, urlMap) {
25
22
  return {
@@ -0,0 +1,12 @@
1
+ import { ISignedMediaUrls } from "../types/db";
2
+ /** Events arrive as table rows and as the event_and_tags composite, so match on shape. */
3
+ export type IEventMediaPaths = {
4
+ file_path?: string | null;
5
+ thumbnail_file_path?: string | null;
6
+ proxy_file_path?: string | null;
7
+ media_url?: string | null;
8
+ };
9
+ /** Collect unique non-empty storage paths used for event media signing. */
10
+ export declare function collectEventStoragePaths(events: Iterable<IEventMediaPaths | null | undefined>): string[];
11
+ /** Attach signed URLs for original, thumbnail, and proxy storage paths. */
12
+ export declare function withEventMediaUrls<T extends IEventMediaPaths>(event: T, urlMap: Map<string, string | null | undefined>): T & ISignedMediaUrls;
@@ -0,0 +1,29 @@
1
+ import { isNonEmptyStorageFilePath, signedUrlForPath } from "./storagePath";
2
+ /** Collect unique non-empty storage paths used for event media signing. */
3
+ export function collectEventStoragePaths(events) {
4
+ const seen = new Set();
5
+ const paths = [];
6
+ for (const event of events) {
7
+ for (const path of [
8
+ event?.file_path,
9
+ event?.thumbnail_file_path,
10
+ event?.proxy_file_path,
11
+ ]) {
12
+ if (isNonEmptyStorageFilePath(path) && !seen.has(path)) {
13
+ seen.add(path);
14
+ paths.push(path);
15
+ }
16
+ }
17
+ }
18
+ return paths;
19
+ }
20
+ /** Attach signed URLs for original, thumbnail, and proxy storage paths. */
21
+ export function withEventMediaUrls(event, urlMap) {
22
+ return {
23
+ ...event,
24
+ // Events predating file_path carry an absolute media_url with nothing to sign.
25
+ media_url: signedUrlForPath(event.file_path, urlMap) ?? event.media_url ?? null,
26
+ thumbnail_url: signedUrlForPath(event.thumbnail_file_path, urlMap),
27
+ proxy_url: signedUrlForPath(event.proxy_file_path, urlMap),
28
+ };
29
+ }
@@ -103,12 +103,16 @@ export declare function server_update_event_lifecycle(event_id: number, lifecycl
103
103
  origin_roll: number | null;
104
104
  processing_blocked_at: string | null;
105
105
  processing_blocked_reason: string | null;
106
+ proxy_file_path: string | null;
107
+ proxy_generated_at: string | null;
106
108
  segmented_at: string | null;
107
109
  sensor_pitch: number | null;
108
110
  sensor_roll: number | null;
109
111
  sensor_yaw: number | null;
110
112
  session_id: number | null;
111
113
  tagged_at: string | null;
114
+ thumbnail_file_path: string | null;
115
+ thumbnail_generated_at: string | null;
112
116
  timestamp_observation: string;
113
117
  tracked_at: string | null;
114
118
  } | null>>;
@@ -1,13 +1,7 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
- import type { IArtifact, IArtifactWithMediaUrl } from "../types/db";
2
+ import type { IArtifact, IArtifactWithMediaUrl, ISignedMediaUrls } from "../types/db";
3
3
  import type { Database } from "../types/supabase";
4
- type EventWithStoragePath = {
5
- file_path?: string | null;
6
- media_url?: string | null;
7
- };
4
+ import { type IEventMediaPaths } from "./eventMedia";
8
5
  export declare function createSignedUrlMap(client: SupabaseClient<Database>, paths: Iterable<string | null | undefined>): Promise<Map<string, string | null>>;
9
- export declare function addEventMediaUrls<T extends EventWithStoragePath>(client: SupabaseClient<Database>, events: T[]): Promise<Array<T & {
10
- media_url: string | null;
11
- }>>;
6
+ export declare function addEventMediaUrls<T extends IEventMediaPaths>(client: SupabaseClient<Database>, events: T[]): Promise<Array<T & ISignedMediaUrls>>;
12
7
  export declare function addArtifactMediaUrls(client: SupabaseClient<Database>, artifacts: IArtifact[]): Promise<IArtifactWithMediaUrl[]>;
13
- export {};
@@ -1,4 +1,5 @@
1
1
  import { collectArtifactStoragePaths, withArtifactMediaUrls, } from "./artifactMedia";
2
+ import { collectEventStoragePaths, withEventMediaUrls, } from "./eventMedia";
2
3
  import { generateSignedUrlsBatchWithClient } from "./storage_internal";
3
4
  import { isNonEmptyStorageFilePath } from "./storagePath";
4
5
  export async function createSignedUrlMap(client, paths) {
@@ -7,13 +8,8 @@ export async function createSignedUrlMap(client, paths) {
7
8
  return new Map(uniquePaths.map((path, index) => [path, urls[index]]));
8
9
  }
9
10
  export async function addEventMediaUrls(client, events) {
10
- const urlMap = await createSignedUrlMap(client, events.map((event) => event.file_path));
11
- return events.map((event) => ({
12
- ...event,
13
- media_url: isNonEmptyStorageFilePath(event.file_path)
14
- ? urlMap.get(event.file_path) ?? event.media_url ?? null
15
- : event.media_url ?? null,
16
- }));
11
+ const urlMap = await createSignedUrlMap(client, collectEventStoragePaths(events));
12
+ return events.map((event) => withEventMediaUrls(event, urlMap));
17
13
  }
18
14
  export async function addArtifactMediaUrls(client, artifacts) {
19
15
  const urlMap = await createSignedUrlMap(client, collectArtifactStoragePaths(artifacts));
@@ -1,5 +1,7 @@
1
1
  /** True when we can sign storage: non-null and not blank after trim. */
2
2
  export declare function isNonEmptyStorageFilePath(path: string | null | undefined): path is string;
3
+ /** Signed URL for a storage path, or null when the path is blank or went unsigned. */
4
+ export declare function signedUrlForPath(path: string | null | undefined, urlMap: Map<string, string | null | undefined>): string | null;
3
5
  /** Assumes DB `file_path` is always `bucket/object/...` */
4
6
  export declare function parseStorageFilePath(filePath: string): {
5
7
  bucket: string;
@@ -2,6 +2,10 @@
2
2
  export function isNonEmptyStorageFilePath(path) {
3
3
  return path != null && path.trim() !== "";
4
4
  }
5
+ /** Signed URL for a storage path, or null when the path is blank or went unsigned. */
6
+ export function signedUrlForPath(path, urlMap) {
7
+ return isNonEmptyStorageFilePath(path) ? urlMap.get(path) ?? null : null;
8
+ }
5
9
  /** Assumes DB `file_path` is always `bucket/object/...` */
6
10
  export function parseStorageFilePath(filePath) {
7
11
  const cleaned = filePath.trim().replace(/^\/+|\/+$/g, "");
@@ -340,6 +340,10 @@ export async function get_event_and_tags_by_event_id_query(client, event_id) {
340
340
  embedded_at: data[0].embedded_at ?? null,
341
341
  segmented_at: data[0].segmented_at ?? null,
342
342
  tagged_at: data[0].tagged_at ?? null,
343
+ thumbnail_file_path: data[0].thumbnail_file_path ?? null,
344
+ thumbnail_generated_at: data[0].thumbnail_generated_at ?? null,
345
+ proxy_file_path: data[0].proxy_file_path ?? null,
346
+ proxy_generated_at: data[0].proxy_generated_at ?? null,
343
347
  };
344
348
  const [eventWithUrl] = await addEventMediaUrls(client, [transformedData]);
345
349
  return IWebResponse.success(eventWithUrl).to_compatible();
@@ -18,10 +18,11 @@ export interface InfiniteRefetchOptions {
18
18
  }
19
19
  export interface InfiniteScrollData<T> {
20
20
  items: T[];
21
- /** True only for the initial load, when no pages are held yet. */
21
+ /** True while a request is in flight and no pages are held: initial load, or a reset. */
22
22
  isLoading: boolean;
23
23
  /** True while a `refetch` is in flight, including a preserve-pages refresh. */
24
24
  isRefreshing: boolean;
25
+ /** True while a later page is being fetched, with earlier pages already shown. */
25
26
  isLoadingMore: boolean;
26
27
  hasMore: boolean;
27
28
  loadMore: () => void;
@@ -51,26 +51,26 @@ function sameCursor(left, right) {
51
51
  }
52
52
  const rowId = (row) => row.id;
53
53
  const feedRowKey = (row) => `${row.sort_ts ?? ""}_${row.sort_id ?? ""}_${row.feed_type ?? ""}`;
54
- /**
55
- * Holds the cursor of the page the live query should request.
56
- *
57
- * State is tagged with `key`, and state carrying a stale key reads as null, so a new entity
58
- * is never requested with the previous entity's cursor.
59
- */
60
- function useCursor(key) {
61
- const [stored, setStored] = useState(() => ({ key, cursor: null }));
62
- const setCursor = useCallback((cursor) => setStored({ key, cursor }), [key]);
63
- return {
64
- key,
65
- cursor: stored.key === key ? stored.cursor : null,
66
- setCursor,
67
- };
68
- }
69
- const noPages = (key) => ({
54
+ const emptyPaging = (key) => ({
70
55
  key,
56
+ cursor: null,
71
57
  pages: [],
72
58
  refreshQueue: null,
73
59
  });
60
+ /**
61
+ * Holds the paging state for `key`, read by the query args and by the engine.
62
+ *
63
+ * State carrying a stale key reads as empty, so a new entity starts from its first page in
64
+ * the same render rather than waiting for an effect to clear the previous one.
65
+ */
66
+ function usePaging(key) {
67
+ const [stored, setStored] = useState(() => emptyPaging(key));
68
+ // Memoized so the empty fallback keeps a stable identity: `items` is derived from
69
+ // `state.pages`, and a fresh array each render would churn consumer dependencies.
70
+ const state = useMemo(() => (stored.key === key ? stored : emptyPaging(key)), [key, stored]);
71
+ const update = useCallback((change) => setStored((previous) => change(previous.key === key ? previous : emptyPaging(key))), [key]);
72
+ return { cursor: state.cursor, state, update };
73
+ }
74
74
  function upsertPage(state, page, rowKey) {
75
75
  const index = state.pages.findIndex((loaded) => sameCursor(loaded.cursor, page.cursor));
76
76
  if (index === -1) {
@@ -97,27 +97,33 @@ const dropRefreshedCursor = (state) => {
97
97
  /**
98
98
  * Accumulates cursor-paginated responses into a list.
99
99
  *
100
- * A single query subscription serves every page: the cursor handle decides which page is
101
- * being requested, and each response is upserted into `pages` under that cursor. A
100
+ * A single query subscription serves every page: the paging state's cursor decides which
101
+ * page is being requested, and each response is upserted into `pages` under that cursor. A
102
102
  * preserve-pages refresh walks the cursor back through the loaded pages one at a time,
103
- * replacing each in place. Pages are tagged with the handle's key, so changing entity or
104
- * filters empties the list and abandons any refresh in the same render.
103
+ * replacing each in place. Changing entity or filters changes the paging key, which empties
104
+ * the list and abandons any refresh in the same render.
105
105
  */
106
- function useInfinitePages({ key, cursor, setCursor }, query, mapping) {
107
- const [stored, setStored] = useState(() => noPages(key));
106
+ function useInfinitePages({ cursor, state, update }, query, mapping) {
108
107
  const latest = useRef({ query, mapping });
109
108
  latest.current = { query, mapping };
110
- const state = stored.key === key ? stored : noPages(key);
111
- const update = useCallback((change) => {
112
- setStored((previous) => change(previous.key === key ? previous : noPages(key)));
113
- }, [key]);
114
- const { data, isLoading } = query;
115
- const page = useMemo(() => (data === undefined ? undefined : latest.current.mapping.toPage(data)), [data]);
109
+ const setCursor = useCallback((next) => update((previous) => sameCursor(previous.cursor, next)
110
+ ? previous
111
+ : { ...previous, cursor: next }), [update]);
112
+ /**
113
+ * Re-requests the page the query currently points at. RTK's `refetch` throws
114
+ * synchronously when the query is skipped, so callers see a rejection instead of a fault
115
+ * at the call site.
116
+ */
117
+ const requestPage = useCallback(async () => latest.current.query.refetch().unwrap(), []);
118
+ const { currentData, isFetching } = query;
119
+ const page = useMemo(() => currentData === undefined
120
+ ? undefined
121
+ : latest.current.mapping.toPage(currentData), [currentData]);
116
122
  useEffect(() => {
117
- if (!page || isLoading)
123
+ if (!page)
118
124
  return;
119
125
  update((previous) => upsertPage(previous, { ...page, cursor }, latest.current.mapping.rowKey));
120
- }, [cursor, isLoading, page, update]);
126
+ }, [cursor, page, update]);
121
127
  useEffect(() => {
122
128
  const queue = state.refreshQueue;
123
129
  if (!queue)
@@ -128,29 +134,31 @@ function useInfinitePages({ key, cursor, setCursor }, query, mapping) {
128
134
  return;
129
135
  }
130
136
  let cancelled = false;
131
- void latest.current.query
132
- .refetch()
133
- .unwrap()
134
- .then((response) => {
135
- if (cancelled)
136
- return;
137
- const { mapping } = latest.current;
138
- const refreshed = mapping.toPage(response);
139
- update((previous) =>
140
- // Only apply while this cursor is still the one being refreshed.
141
- previous.refreshQueue &&
142
- sameCursor(previous.refreshQueue[0], refreshing)
143
- ? dropRefreshedCursor(upsertPage(previous, { ...refreshed, cursor: refreshing }, mapping.rowKey))
144
- : previous);
145
- })
146
- .catch(() => {
147
- if (!cancelled)
148
- update(dropRefreshedCursor);
149
- });
137
+ // A refresh that cannot run drops its cursor and moves on, so a rejection never leaves
138
+ // the queue stuck.
139
+ void (async () => {
140
+ try {
141
+ const response = await requestPage();
142
+ if (cancelled)
143
+ return;
144
+ const { mapping } = latest.current;
145
+ const refreshed = mapping.toPage(response);
146
+ update((previous) =>
147
+ // Only apply while this cursor is still the one being refreshed.
148
+ previous.refreshQueue &&
149
+ sameCursor(previous.refreshQueue[0], refreshing)
150
+ ? dropRefreshedCursor(upsertPage(previous, { ...refreshed, cursor: refreshing }, mapping.rowKey))
151
+ : previous);
152
+ }
153
+ catch {
154
+ if (!cancelled)
155
+ update(dropRefreshedCursor);
156
+ }
157
+ })();
150
158
  return () => {
151
159
  cancelled = true;
152
160
  };
153
- }, [cursor, setCursor, state.refreshQueue, update]);
161
+ }, [cursor, requestPage, setCursor, state.refreshQueue, update]);
154
162
  /** Both modes queue the cursors to re-request, so a refetch always reaches the network. */
155
163
  const refetch = useCallback((options) => {
156
164
  update((previous) => options?.preservePages && previous.pages.length > 0
@@ -158,17 +166,32 @@ function useInfinitePages({ key, cursor, setCursor }, query, mapping) {
158
166
  ...previous,
159
167
  refreshQueue: previous.pages.map((loaded) => loaded.cursor),
160
168
  }
161
- : { ...noPages(key), refreshQueue: [null] });
162
- }, [key, update]);
169
+ : { ...emptyPaging(previous.key), refreshQueue: [null] });
170
+ }, [update]);
163
171
  const lastPage = state.pages[state.pages.length - 1];
164
172
  const nextCursor = lastPage?.nextCursor ?? null;
165
173
  const hasMore = Boolean(lastPage?.hasMore) && nextCursor !== null;
166
174
  const isRefreshing = state.refreshQueue !== null;
167
175
  const loadMore = useCallback(() => {
168
- if (!hasMore || nextCursor === null || isLoading || isRefreshing)
176
+ if (!hasMore || nextCursor === null || isFetching || isRefreshing)
177
+ return;
178
+ // The cursor already resting on `nextCursor` means that page's request failed, since a
179
+ // loaded page would have moved `nextCursor` on. Re-setting it would be a no-op, so the
180
+ // failed request is retried instead of leaving the list unable to advance.
181
+ if (sameCursor(cursor, nextCursor)) {
182
+ void requestPage().catch(() => { });
169
183
  return;
184
+ }
170
185
  setCursor(nextCursor);
171
- }, [hasMore, isLoading, isRefreshing, nextCursor, setCursor]);
186
+ }, [
187
+ cursor,
188
+ hasMore,
189
+ isFetching,
190
+ isRefreshing,
191
+ nextCursor,
192
+ requestPage,
193
+ setCursor,
194
+ ]);
172
195
  const items = useMemo(() => {
173
196
  const seen = new Set();
174
197
  return state.pages
@@ -185,9 +208,9 @@ function useInfinitePages({ key, cursor, setCursor }, query, mapping) {
185
208
  }, [state.pages]);
186
209
  return {
187
210
  items,
188
- isLoading: isLoading && state.pages.length === 0,
211
+ isLoading: state.pages.length === 0 && (isFetching || isRefreshing),
189
212
  isRefreshing,
190
- isLoadingMore: isLoading && state.pages.length > 0,
213
+ isLoadingMore: isFetching && state.pages.length > 0 && !isRefreshing,
191
214
  hasMore,
192
215
  loadMore,
193
216
  refetch,
@@ -261,7 +284,7 @@ const analysisTasksPage = (response) => ({
261
284
  // =====================================================
262
285
  export const useInfiniteSessionsByHerd = (herdId, options) => {
263
286
  const filtersKey = useFiltersKey(options);
264
- const paging = useCursor(`herd:${herdId}|${filtersKey}`);
287
+ const paging = usePaging(`herd:${herdId}|${filtersKey}`);
265
288
  const query = useGetSessionsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...flightArgs(options) }, { skip: !options.enabled || !herdId });
266
289
  return useInfinitePages(paging, query, {
267
290
  toPage: sessionsPage,
@@ -270,7 +293,7 @@ export const useInfiniteSessionsByHerd = (herdId, options) => {
270
293
  };
271
294
  export const useInfiniteSessionsByDevice = (deviceId, options) => {
272
295
  const filtersKey = useFiltersKey(options);
273
- const paging = useCursor(`device:${deviceId}|${filtersKey}`);
296
+ const paging = usePaging(`device:${deviceId}|${filtersKey}`);
274
297
  const query = useGetSessionsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...flightArgs(options) }, { skip: !options.enabled || !deviceId });
275
298
  return useInfinitePages(paging, query, {
276
299
  toPage: sessionsPage,
@@ -282,7 +305,7 @@ export const useInfiniteSessionsByDevice = (deviceId, options) => {
282
305
  // =====================================================
283
306
  export const useInfiniteEventsByHerd = (herdId, options) => {
284
307
  const filtersKey = useFiltersKey(options);
285
- const paging = useCursor(`herd:${herdId}|${filtersKey}`);
308
+ const paging = usePaging(`herd:${herdId}|${filtersKey}`);
286
309
  const query = useGetEventsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
287
310
  return useInfinitePages(paging, query, {
288
311
  toPage: eventsPage,
@@ -291,7 +314,7 @@ export const useInfiniteEventsByHerd = (herdId, options) => {
291
314
  };
292
315
  export const useInfiniteEventsByDevice = (deviceId, options) => {
293
316
  const filtersKey = useFiltersKey(options);
294
- const paging = useCursor(`device:${deviceId}|${filtersKey}`);
317
+ const paging = usePaging(`device:${deviceId}|${filtersKey}`);
295
318
  const query = useGetEventsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
296
319
  return useInfinitePages(paging, query, {
297
320
  toPage: eventsPage,
@@ -303,7 +326,7 @@ export const useInfiniteEventsByDevice = (deviceId, options) => {
303
326
  // =====================================================
304
327
  export const useInfiniteArtifactsByHerd = (herdId, options) => {
305
328
  const filtersKey = useFiltersKey(options);
306
- const paging = useCursor(`herd:${herdId}|${filtersKey}`);
329
+ const paging = usePaging(`herd:${herdId}|${filtersKey}`);
307
330
  const query = useGetArtifactsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
308
331
  return useInfinitePages(paging, query, {
309
332
  toPage: artifactsPage,
@@ -312,7 +335,7 @@ export const useInfiniteArtifactsByHerd = (herdId, options) => {
312
335
  };
313
336
  export const useInfiniteArtifactsByDevice = (deviceId, options) => {
314
337
  const filtersKey = useFiltersKey(options);
315
- const paging = useCursor(`device:${deviceId}|${filtersKey}`);
338
+ const paging = usePaging(`device:${deviceId}|${filtersKey}`);
316
339
  const query = useGetArtifactsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
317
340
  return useInfinitePages(paging, query, {
318
341
  toPage: artifactsPage,
@@ -324,7 +347,7 @@ export const useInfiniteArtifactsByDevice = (deviceId, options) => {
324
347
  // =====================================================
325
348
  export const useInfiniteFeedByHerd = (herdId, options) => {
326
349
  const filtersKey = useFiltersKey(options);
327
- const paging = useCursor(`herd:${herdId}|${filtersKey}`);
350
+ const paging = usePaging(`herd:${herdId}|${filtersKey}`);
328
351
  const query = useGetFeedInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
329
352
  return useInfinitePages(paging, query, {
330
353
  toPage: feedPage,
@@ -333,7 +356,7 @@ export const useInfiniteFeedByHerd = (herdId, options) => {
333
356
  };
334
357
  export const useInfiniteFeedByDevice = (deviceId, options) => {
335
358
  const filtersKey = useFiltersKey(options);
336
- const paging = useCursor(`device:${deviceId}|${filtersKey}`);
359
+ const paging = usePaging(`device:${deviceId}|${filtersKey}`);
337
360
  const query = useGetFeedInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
338
361
  return useInfinitePages(paging, query, {
339
362
  toPage: feedPage,
@@ -342,7 +365,7 @@ export const useInfiniteFeedByDevice = (deviceId, options) => {
342
365
  };
343
366
  export const useInfiniteAnalysisJobs = (options) => {
344
367
  const status = options.status ?? null;
345
- const paging = useCursor(`status:${status}`);
368
+ const paging = usePaging(`status:${status}`);
346
369
  const query = useGetAnalysisJobsInfiniteQuery({
347
370
  cursor: paging.cursor,
348
371
  status,
@@ -357,7 +380,7 @@ export const useInfiniteAnalysisJobs = (options) => {
357
380
  export const useInfiniteAnalysisTasks = (options) => {
358
381
  const status = options.status ?? null;
359
382
  const jobId = options.job_id ?? null;
360
- const paging = useCursor(`job:${jobId}|status:${status}`);
383
+ const paging = usePaging(`job:${jobId}|status:${status}`);
361
384
  const query = useGetAnalysisTasksInfiniteQuery({
362
385
  cursor: paging.cursor,
363
386
  status,
@@ -4,6 +4,7 @@ export * from "../types";
4
4
  export * from "../helpers/artifactMedia";
5
5
  export * from "../helpers/bounding_boxes";
6
6
  export * from "../helpers/email";
7
+ export * from "../helpers/eventMedia";
7
8
  export * from "../helpers/eventUtils";
8
9
  export { default as get_gps_center } from "../helpers/gps";
9
10
  export * from "../helpers/herd_modules_equal";
@@ -6,6 +6,7 @@ export * from "../types";
6
6
  export * from "../helpers/artifactMedia";
7
7
  export * from "../helpers/bounding_boxes";
8
8
  export * from "../helpers/email";
9
+ export * from "../helpers/eventMedia";
9
10
  export * from "../helpers/eventUtils";
10
11
  export { default as get_gps_center } from "../helpers/gps";
11
12
  export * from "../helpers/herd_modules_equal";
@@ -151,7 +151,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
151
151
  isFetching: false;
152
152
  isSuccess: false;
153
153
  isError: false;
154
- }, "data" | "isLoading" | "isFetching"> & {
154
+ }, "data" | "isFetching" | "isLoading"> & {
155
155
  isLoading: true;
156
156
  isFetching: boolean;
157
157
  data: undefined;
@@ -260,7 +260,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
260
260
  isFetching: false;
261
261
  isSuccess: false;
262
262
  isError: false;
263
- }, "data" | "isLoading" | "isFetching"> & {
263
+ }, "data" | "isFetching" | "isLoading"> & {
264
264
  isLoading: true;
265
265
  isFetching: boolean;
266
266
  data: undefined;
@@ -367,7 +367,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
367
367
  isFetching: false;
368
368
  isSuccess: false;
369
369
  isError: false;
370
- }, "data" | "isLoading" | "isFetching"> & {
370
+ }, "data" | "isFetching" | "isLoading"> & {
371
371
  isLoading: true;
372
372
  isFetching: boolean;
373
373
  data: undefined;
@@ -476,7 +476,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
476
476
  isFetching: false;
477
477
  isSuccess: false;
478
478
  isError: false;
479
- }, "data" | "isLoading" | "isFetching"> & {
479
+ }, "data" | "isFetching" | "isLoading"> & {
480
480
  isLoading: true;
481
481
  isFetching: boolean;
482
482
  data: undefined;
@@ -583,7 +583,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
583
583
  isFetching: false;
584
584
  isSuccess: false;
585
585
  isError: false;
586
- }, "data" | "isLoading" | "isFetching"> & {
586
+ }, "data" | "isFetching" | "isLoading"> & {
587
587
  isLoading: true;
588
588
  isFetching: boolean;
589
589
  data: undefined;
@@ -692,7 +692,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
692
692
  isFetching: false;
693
693
  isSuccess: false;
694
694
  isError: false;
695
- }, "data" | "isLoading" | "isFetching"> & {
695
+ }, "data" | "isFetching" | "isLoading"> & {
696
696
  isLoading: true;
697
697
  isFetching: boolean;
698
698
  data: undefined;
@@ -799,7 +799,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
799
799
  isFetching: false;
800
800
  isSuccess: false;
801
801
  isError: false;
802
- }, "data" | "isLoading" | "isFetching"> & {
802
+ }, "data" | "isFetching" | "isLoading"> & {
803
803
  isLoading: true;
804
804
  isFetching: boolean;
805
805
  data: undefined;
@@ -908,7 +908,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
908
908
  isFetching: false;
909
909
  isSuccess: false;
910
910
  isError: false;
911
- }, "data" | "isLoading" | "isFetching"> & {
911
+ }, "data" | "isFetching" | "isLoading"> & {
912
912
  isLoading: true;
913
913
  isFetching: boolean;
914
914
  data: undefined;
@@ -1015,7 +1015,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1015
1015
  isFetching: false;
1016
1016
  isSuccess: false;
1017
1017
  isError: false;
1018
- }, "data" | "isLoading" | "isFetching"> & {
1018
+ }, "data" | "isFetching" | "isLoading"> & {
1019
1019
  isLoading: true;
1020
1020
  isFetching: boolean;
1021
1021
  data: undefined;
@@ -1124,7 +1124,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1124
1124
  isFetching: false;
1125
1125
  isSuccess: false;
1126
1126
  isError: false;
1127
- }, "data" | "isLoading" | "isFetching"> & {
1127
+ }, "data" | "isFetching" | "isLoading"> & {
1128
1128
  isLoading: true;
1129
1129
  isFetching: boolean;
1130
1130
  data: undefined;
@@ -1231,7 +1231,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1231
1231
  isFetching: false;
1232
1232
  isSuccess: false;
1233
1233
  isError: false;
1234
- }, "data" | "isLoading" | "isFetching"> & {
1234
+ }, "data" | "isFetching" | "isLoading"> & {
1235
1235
  isLoading: true;
1236
1236
  isFetching: boolean;
1237
1237
  data: undefined;
@@ -1340,7 +1340,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1340
1340
  isFetching: false;
1341
1341
  isSuccess: false;
1342
1342
  isError: false;
1343
- }, "data" | "isLoading" | "isFetching"> & {
1343
+ }, "data" | "isFetching" | "isLoading"> & {
1344
1344
  isLoading: true;
1345
1345
  isFetching: boolean;
1346
1346
  data: undefined;
@@ -1445,7 +1445,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1445
1445
  isFetching: false;
1446
1446
  isSuccess: false;
1447
1447
  isError: false;
1448
- }, "data" | "isLoading" | "isFetching"> & {
1448
+ }, "data" | "isFetching" | "isLoading"> & {
1449
1449
  isLoading: true;
1450
1450
  isFetching: boolean;
1451
1451
  data: undefined;
@@ -1538,7 +1538,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1538
1538
  isFetching: false;
1539
1539
  isSuccess: false;
1540
1540
  isError: false;
1541
- }, "data" | "isLoading" | "isFetching"> & {
1541
+ }, "data" | "isFetching" | "isLoading"> & {
1542
1542
  isLoading: true;
1543
1543
  isFetching: boolean;
1544
1544
  data: undefined;
@@ -1629,7 +1629,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1629
1629
  isFetching: false;
1630
1630
  isSuccess: false;
1631
1631
  isError: false;
1632
- }, "data" | "isLoading" | "isFetching"> & {
1632
+ }, "data" | "isFetching" | "isLoading"> & {
1633
1633
  isLoading: true;
1634
1634
  isFetching: boolean;
1635
1635
  data: undefined;
@@ -1722,7 +1722,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1722
1722
  isFetching: false;
1723
1723
  isSuccess: false;
1724
1724
  isError: false;
1725
- }, "data" | "isLoading" | "isFetching"> & {
1725
+ }, "data" | "isFetching" | "isLoading"> & {
1726
1726
  isLoading: true;
1727
1727
  isFetching: boolean;
1728
1728
  data: undefined;
@@ -1813,7 +1813,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1813
1813
  isFetching: false;
1814
1814
  isSuccess: false;
1815
1815
  isError: false;
1816
- }, "data" | "isLoading" | "isFetching"> & {
1816
+ }, "data" | "isFetching" | "isLoading"> & {
1817
1817
  isLoading: true;
1818
1818
  isFetching: boolean;
1819
1819
  data: undefined;
@@ -1906,7 +1906,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1906
1906
  isFetching: false;
1907
1907
  isSuccess: false;
1908
1908
  isError: false;
1909
- }, "data" | "isLoading" | "isFetching"> & {
1909
+ }, "data" | "isFetching" | "isLoading"> & {
1910
1910
  isLoading: true;
1911
1911
  isFetching: boolean;
1912
1912
  data: undefined;
@@ -1997,7 +1997,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
1997
1997
  isFetching: false;
1998
1998
  isSuccess: false;
1999
1999
  isError: false;
2000
- }, "data" | "isLoading" | "isFetching"> & {
2000
+ }, "data" | "isFetching" | "isLoading"> & {
2001
2001
  isLoading: true;
2002
2002
  isFetching: boolean;
2003
2003
  data: undefined;
@@ -2090,7 +2090,7 @@ export declare const useGetSessionsInfiniteByHerdQuery: <R extends Record<string
2090
2090
  isFetching: false;
2091
2091
  isSuccess: false;
2092
2092
  isError: false;
2093
- }, "data" | "isLoading" | "isFetching"> & {
2093
+ }, "data" | "isFetching" | "isLoading"> & {
2094
2094
  isLoading: true;
2095
2095
  isFetching: boolean;
2096
2096
  data: undefined;
package/dist/store/api.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
3
3
  import { addArtifactMediaUrls, addEventMediaUrls, createSignedUrlMap, } from "../helpers/media_urls";
4
4
  import { collectArtifactStoragePaths, withArtifactMediaUrls, } from "../helpers/artifactMedia";
5
- import { isNonEmptyStorageFilePath } from "../helpers/storagePath";
5
+ import { collectEventStoragePaths, withEventMediaUrls, } from "../helpers/eventMedia";
6
6
  async function signedUrlMapForFeedRows(supabase, rows) {
7
7
  return createSignedUrlMap(supabase, rows.flatMap((row) => [
8
- row.event_data?.file_path,
8
+ ...collectEventStoragePaths([row.event_data]),
9
9
  ...(row.artifact_data
10
10
  ? collectArtifactStoragePaths([row.artifact_data])
11
11
  : []),
@@ -434,12 +434,7 @@ export const scoutApi = createApi({
434
434
  const items = resultRows.map((row) => ({
435
435
  ...row,
436
436
  event_data: row.event_data
437
- ? {
438
- ...row.event_data,
439
- media_url: isNonEmptyStorageFilePath(row.event_data.file_path)
440
- ? urlMap.get(row.event_data.file_path) ?? null
441
- : null,
442
- }
437
+ ? withEventMediaUrls(row.event_data, urlMap)
443
438
  : null,
444
439
  artifact_data: row.artifact_data
445
440
  ? withArtifactMediaUrls(row.artifact_data, urlMap)
@@ -504,12 +499,7 @@ export const scoutApi = createApi({
504
499
  const items = resultRows.map((row) => ({
505
500
  ...row,
506
501
  event_data: row.event_data
507
- ? {
508
- ...row.event_data,
509
- media_url: isNonEmptyStorageFilePath(row.event_data.file_path)
510
- ? urlMap.get(row.event_data.file_path) ?? null
511
- : null,
512
- }
502
+ ? withEventMediaUrls(row.event_data, urlMap)
513
503
  : null,
514
504
  artifact_data: row.artifact_data
515
505
  ? withArtifactMediaUrls(row.artifact_data, urlMap)
@@ -217,14 +217,13 @@ export type BatteryCycleCountUpdate = Database["public"]["Tables"]["battery_cycl
217
217
  export type IMinimalSubjectForArtifact = Database["public"]["Functions"]["get_minimal_subjects_for_artifact"]["Returns"][number];
218
218
  export type IAnalysisJob = Database["public"]["Tables"]["analysis_jobs"]["Row"];
219
219
  export type IAnalysisTask = Database["public"]["Tables"]["analysis_tasks"]["Row"];
220
- export type IArtifactWithMediaUrl = IArtifact & {
220
+ export type ISignedMediaUrls = {
221
221
  media_url?: string | null;
222
222
  thumbnail_url?: string | null;
223
223
  proxy_url?: string | null;
224
224
  };
225
- export type IEventWithMediaUrl = IEvent & {
226
- media_url?: string | null;
227
- };
225
+ export type IArtifactWithMediaUrl = IArtifact & ISignedMediaUrls;
226
+ export type IEventWithMediaUrl = IEvent & ISignedMediaUrls;
228
227
  export type IVersionsSoftwareWithBuildUrl = IVersionsSoftware & {
229
228
  build_artifact_url?: string | null;
230
229
  };
@@ -368,9 +367,7 @@ export type IFeedItem = {
368
367
  feed_type: string | null;
369
368
  sort_ts: string | null;
370
369
  sort_id: number | null;
371
- event_data: (IEventAndTagsPrettyLocation & {
372
- media_url?: string | null;
373
- }) | null;
370
+ event_data: (IEventAndTagsPrettyLocation & ISignedMediaUrls) | null;
374
371
  artifact_data: IArtifactWithMediaUrl | null;
375
372
  };
376
373
  export type ISessionWithCoordinates = Database["public"]["CompositeTypes"]["session_with_coordinates"];
@@ -1504,12 +1504,16 @@ export type Database = {
1504
1504
  origin_roll: number | null;
1505
1505
  processing_blocked_at: string | null;
1506
1506
  processing_blocked_reason: string | null;
1507
+ proxy_file_path: string | null;
1508
+ proxy_generated_at: string | null;
1507
1509
  segmented_at: string | null;
1508
1510
  sensor_pitch: number | null;
1509
1511
  sensor_roll: number | null;
1510
1512
  sensor_yaw: number | null;
1511
1513
  session_id: number | null;
1512
1514
  tagged_at: string | null;
1515
+ thumbnail_file_path: string | null;
1516
+ thumbnail_generated_at: string | null;
1513
1517
  timestamp_observation: string;
1514
1518
  tracked_at: string | null;
1515
1519
  };
@@ -1538,12 +1542,16 @@ export type Database = {
1538
1542
  origin_roll?: number | null;
1539
1543
  processing_blocked_at?: string | null;
1540
1544
  processing_blocked_reason?: string | null;
1545
+ proxy_file_path?: string | null;
1546
+ proxy_generated_at?: string | null;
1541
1547
  segmented_at?: string | null;
1542
1548
  sensor_pitch?: number | null;
1543
1549
  sensor_roll?: number | null;
1544
1550
  sensor_yaw?: number | null;
1545
1551
  session_id?: number | null;
1546
1552
  tagged_at?: string | null;
1553
+ thumbnail_file_path?: string | null;
1554
+ thumbnail_generated_at?: string | null;
1547
1555
  timestamp_observation?: string;
1548
1556
  tracked_at?: string | null;
1549
1557
  };
@@ -1572,12 +1580,16 @@ export type Database = {
1572
1580
  origin_roll?: number | null;
1573
1581
  processing_blocked_at?: string | null;
1574
1582
  processing_blocked_reason?: string | null;
1583
+ proxy_file_path?: string | null;
1584
+ proxy_generated_at?: string | null;
1575
1585
  segmented_at?: string | null;
1576
1586
  sensor_pitch?: number | null;
1577
1587
  sensor_roll?: number | null;
1578
1588
  sensor_yaw?: number | null;
1579
1589
  session_id?: number | null;
1580
1590
  tagged_at?: string | null;
1591
+ thumbnail_file_path?: string | null;
1592
+ thumbnail_generated_at?: string | null;
1581
1593
  timestamp_observation?: string;
1582
1594
  tracked_at?: string | null;
1583
1595
  };
@@ -7015,6 +7027,10 @@ export type Database = {
7015
7027
  embedded_at: string | null;
7016
7028
  segmented_at: string | null;
7017
7029
  tagged_at: string | null;
7030
+ thumbnail_file_path: string | null;
7031
+ thumbnail_generated_at: string | null;
7032
+ proxy_file_path: string | null;
7033
+ proxy_generated_at: string | null;
7018
7034
  };
7019
7035
  event_plus_tags: {
7020
7036
  id: number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "exports": {
6
6
  "./client": {