@adventurelabs/scout-core 2.0.18 → 2.1.1
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 +78 -0
- package/package.json +4 -2
|
@@ -10,18 +10,27 @@ const OPERATIONS = {
|
|
|
10
10
|
};
|
|
11
11
|
const DEFAULT_BACKOFF_INITIAL_MS = 1000;
|
|
12
12
|
const DEFAULT_BACKOFF_MAX_MS = 60000;
|
|
13
|
+
/** A channel still joining reports its own outcome, so only a dead one needs a rejoin. */
|
|
14
|
+
const HEALTHY_CHANNEL_STATES = new Set([
|
|
15
|
+
"joined",
|
|
16
|
+
"joining",
|
|
17
|
+
]);
|
|
13
18
|
function isRealtimeAuthError(error) {
|
|
14
19
|
const message = error?.message ?? "";
|
|
15
20
|
return /unauthorized|permission/i.test(message);
|
|
16
21
|
}
|
|
22
|
+
/** Jittered, so a server blip does not bring every topic and every client back in lockstep. */
|
|
17
23
|
function backoffDelayMs(attempt, initialMs, maxMs) {
|
|
18
|
-
|
|
24
|
+
const ceiling = Math.min(maxMs, initialMs * 2 ** Math.max(0, attempt));
|
|
25
|
+
return ceiling * (0.5 + Math.random() * 0.5);
|
|
19
26
|
}
|
|
20
27
|
export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, rowFilter) {
|
|
21
|
-
const { authErrorRetry = "stop", backoffInitialMs = DEFAULT_BACKOFF_INITIAL_MS, backoffMaxMs = DEFAULT_BACKOFF_MAX_MS, backoffMaxAttempts = Number.POSITIVE_INFINITY, } = options;
|
|
28
|
+
const { authErrorRetry = "stop", backoffInitialMs = DEFAULT_BACKOFF_INITIAL_MS, backoffMaxMs = DEFAULT_BACKOFF_MAX_MS, backoffMaxAttempts = Number.POSITIVE_INFINITY, onMissedUpdates, recoverOnFocus = true, } = options;
|
|
22
29
|
const [latestUpdate, setLatestUpdate] = useState(null);
|
|
23
30
|
const rowFilterRef = useRef(rowFilter);
|
|
24
31
|
rowFilterRef.current = rowFilter;
|
|
32
|
+
const onMissedUpdatesRef = useRef(onMissedUpdates);
|
|
33
|
+
onMissedUpdatesRef.current = onMissedUpdates;
|
|
25
34
|
const subscribedTopics = useAuthorizedRealtimeTopics(topics);
|
|
26
35
|
const clearLatestUpdate = useCallback(() => setLatestUpdate(null), []);
|
|
27
36
|
useEffect(() => {
|
|
@@ -31,7 +40,12 @@ export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, r
|
|
|
31
40
|
let cancelled = false;
|
|
32
41
|
const channels = new Map();
|
|
33
42
|
const authAttempts = new Map();
|
|
43
|
+
const dropAttempts = new Map();
|
|
34
44
|
const blockedTopics = new Set();
|
|
45
|
+
/** Topics being rejoined after a drop, so their next join reports missed updates. */
|
|
46
|
+
const droppedTopics = new Set();
|
|
47
|
+
/** Topics a channel already exists for, which is what tells a drop from a first join. */
|
|
48
|
+
const startedTopics = new Set();
|
|
35
49
|
const retryTimers = new Map();
|
|
36
50
|
const removeTopicChannel = (topic) => {
|
|
37
51
|
const channel = channels.get(topic);
|
|
@@ -40,13 +54,13 @@ export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, r
|
|
|
40
54
|
channels.delete(topic);
|
|
41
55
|
void scoutSupabase.removeChannel(channel);
|
|
42
56
|
};
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
57
|
+
const scheduleRejoin = (topic, attempts) => {
|
|
58
|
+
if (cancelled)
|
|
45
59
|
return;
|
|
46
|
-
const attempt =
|
|
60
|
+
const attempt = attempts.get(topic) ?? 0;
|
|
47
61
|
if (attempt >= backoffMaxAttempts)
|
|
48
62
|
return;
|
|
49
|
-
|
|
63
|
+
attempts.set(topic, attempt + 1);
|
|
50
64
|
const existing = retryTimers.get(topic);
|
|
51
65
|
if (existing)
|
|
52
66
|
clearTimeout(existing);
|
|
@@ -71,20 +85,39 @@ export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, r
|
|
|
71
85
|
setLatestUpdate({ data: row, operation, table: change.table });
|
|
72
86
|
});
|
|
73
87
|
channels.set(topic, channel);
|
|
88
|
+
startedTopics.add(topic);
|
|
74
89
|
channel.subscribe((status, error) => {
|
|
75
90
|
if (status === "SUBSCRIBED") {
|
|
76
91
|
authAttempts.delete(topic);
|
|
92
|
+
dropAttempts.delete(topic);
|
|
77
93
|
blockedTopics.delete(topic);
|
|
94
|
+
if (droppedTopics.delete(topic))
|
|
95
|
+
onMissedUpdatesRef.current?.();
|
|
78
96
|
return;
|
|
79
97
|
}
|
|
80
|
-
|
|
98
|
+
// A status for a channel we replaced or tore down says nothing about the topic now.
|
|
99
|
+
if (cancelled || channels.get(topic) !== channel)
|
|
81
100
|
return;
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
if (status === "CHANNEL_ERROR" && isRealtimeAuthError(error)) {
|
|
102
|
+
console.warn(`[scout-core realtime] 🟡 ${topic} unavailable`, error?.message ?? "");
|
|
103
|
+
blockedTopics.add(topic);
|
|
104
|
+
removeTopicChannel(topic);
|
|
105
|
+
// Rows published while unauthorized are gone, so a rejoin still owes a refetch.
|
|
106
|
+
droppedTopics.add(topic);
|
|
107
|
+
if (authErrorRetry === "backoff")
|
|
108
|
+
scheduleRejoin(topic, authAttempts);
|
|
84
109
|
return;
|
|
85
|
-
|
|
110
|
+
}
|
|
111
|
+
if (status !== "CHANNEL_ERROR" &&
|
|
112
|
+
status !== "CLOSED" &&
|
|
113
|
+
status !== "TIMED_OUT") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Rejoining is the only way to see rows published during a transport drop.
|
|
117
|
+
console.warn(`[scout-core realtime] 🟡 ${topic} dropped (${status})`, error?.message ?? "");
|
|
86
118
|
removeTopicChannel(topic);
|
|
87
|
-
|
|
119
|
+
droppedTopics.add(topic);
|
|
120
|
+
scheduleRejoin(topic, dropAttempts);
|
|
88
121
|
});
|
|
89
122
|
};
|
|
90
123
|
async function authenticateAndJoin(topicsToJoin) {
|
|
@@ -116,10 +149,41 @@ export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, r
|
|
|
116
149
|
retryBlockedTopics();
|
|
117
150
|
}
|
|
118
151
|
});
|
|
152
|
+
/** A socket that died while hidden reports nothing, so returning rejoins dead topics. */
|
|
153
|
+
const recoverDroppedTopics = () => {
|
|
154
|
+
const offline = typeof navigator !== "undefined" && !navigator.onLine;
|
|
155
|
+
if (cancelled || offline)
|
|
156
|
+
return;
|
|
157
|
+
const toRejoin = subscribedTopics.filter((topic) => startedTopics.has(topic) &&
|
|
158
|
+
!blockedTopics.has(topic) &&
|
|
159
|
+
!HEALTHY_CHANNEL_STATES.has(channels.get(topic)?.state ?? "") &&
|
|
160
|
+
!retryTimers.has(topic));
|
|
161
|
+
if (toRejoin.length === 0)
|
|
162
|
+
return;
|
|
163
|
+
toRejoin.forEach((topic) => {
|
|
164
|
+
removeTopicChannel(topic);
|
|
165
|
+
droppedTopics.add(topic);
|
|
166
|
+
dropAttempts.delete(topic);
|
|
167
|
+
});
|
|
168
|
+
void authenticateAndJoin(toRejoin);
|
|
169
|
+
};
|
|
170
|
+
const handleVisibilityChange = () => {
|
|
171
|
+
if (document.visibilityState === "visible")
|
|
172
|
+
recoverDroppedTopics();
|
|
173
|
+
};
|
|
174
|
+
const listeningForFocus = recoverOnFocus && typeof window !== "undefined";
|
|
175
|
+
if (listeningForFocus) {
|
|
176
|
+
window.addEventListener("online", recoverDroppedTopics);
|
|
177
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
178
|
+
}
|
|
119
179
|
void authenticateAndJoin(subscribedTopics);
|
|
120
180
|
return () => {
|
|
121
181
|
cancelled = true;
|
|
122
182
|
unsubscribeAuth();
|
|
183
|
+
if (listeningForFocus) {
|
|
184
|
+
window.removeEventListener("online", recoverDroppedTopics);
|
|
185
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
186
|
+
}
|
|
123
187
|
retryTimers.forEach((timer) => clearTimeout(timer));
|
|
124
188
|
retryTimers.clear();
|
|
125
189
|
channels.forEach((channel) => {
|
|
@@ -134,6 +198,7 @@ export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, r
|
|
|
134
198
|
backoffInitialMs,
|
|
135
199
|
backoffMaxMs,
|
|
136
200
|
backoffMaxAttempts,
|
|
201
|
+
recoverOnFocus,
|
|
137
202
|
]);
|
|
138
203
|
return [latestUpdate, clearLatestUpdate];
|
|
139
204
|
}
|
|
@@ -100,7 +100,7 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
100
100
|
const isCurrent = () => refreshIdRef.current === refreshId;
|
|
101
101
|
const startTime = Date.now();
|
|
102
102
|
const isOffline = typeof navigator === "undefined" || !navigator.onLine;
|
|
103
|
-
let
|
|
103
|
+
let cachedSnapshot = null;
|
|
104
104
|
let cacheReady = false;
|
|
105
105
|
const complete = (loadingState) => {
|
|
106
106
|
if (!isCurrent())
|
|
@@ -114,7 +114,7 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
114
114
|
if (activeUserIdRef.current &&
|
|
115
115
|
activeUserIdRef.current !== userId) {
|
|
116
116
|
resetClientState();
|
|
117
|
-
|
|
117
|
+
cachedSnapshot = null;
|
|
118
118
|
}
|
|
119
119
|
activeUserIdRef.current = userId;
|
|
120
120
|
};
|
|
@@ -141,7 +141,7 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
141
141
|
if (cacheResult.data === null) {
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
cachedSnapshot = cacheResult.data;
|
|
145
145
|
dispatch(setDataSource(EnumDataSource.CACHE));
|
|
146
146
|
dispatch(setDataSourceInfo({
|
|
147
147
|
source: EnumDataSource.CACHE,
|
|
@@ -149,7 +149,10 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
149
149
|
cacheAge: cacheResult.age,
|
|
150
150
|
isStale: cacheResult.isStale,
|
|
151
151
|
}));
|
|
152
|
-
dispatchInTransition(dispatch, setHerdModules({
|
|
152
|
+
dispatchInTransition(dispatch, setHerdModules({
|
|
153
|
+
modules: cachedSnapshot.herd_modules,
|
|
154
|
+
productCatalog: cachedSnapshot.product_catalog,
|
|
155
|
+
}));
|
|
153
156
|
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
|
|
154
157
|
}
|
|
155
158
|
catch (cacheError) {
|
|
@@ -188,14 +191,14 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
188
191
|
dispatch(setUserApiDuration(sessionDuration));
|
|
189
192
|
dispatchInTransition(dispatch, setUser(sessionUser));
|
|
190
193
|
}
|
|
191
|
-
if (
|
|
194
|
+
if (cachedSnapshot === null) {
|
|
192
195
|
dispatch(setDataSource(EnumDataSource.UNKNOWN));
|
|
193
196
|
dispatch(setDataSourceInfo({
|
|
194
197
|
source: EnumDataSource.UNKNOWN,
|
|
195
198
|
timestamp: Date.now(),
|
|
196
199
|
}));
|
|
197
200
|
}
|
|
198
|
-
complete(
|
|
201
|
+
complete(cachedSnapshot !== null
|
|
199
202
|
? EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED
|
|
200
203
|
: EnumHerdModulesLoadingState.UNSUCCESSFULLY_LOADED);
|
|
201
204
|
return;
|
|
@@ -226,16 +229,19 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
226
229
|
!Array.isArray(herdModulesResponse.data)) {
|
|
227
230
|
throw new Error("Invalid herd modules response");
|
|
228
231
|
}
|
|
229
|
-
const
|
|
232
|
+
const freshSnapshot = {
|
|
233
|
+
herd_modules: herdModulesResponse.data,
|
|
234
|
+
product_catalog: herdModulesResponse.product_catalog,
|
|
235
|
+
};
|
|
230
236
|
const freshContentHash = herdModulesResponse.content_hash;
|
|
231
237
|
if (cacheReady) {
|
|
232
238
|
try {
|
|
233
|
-
await scoutCache.setHerdModules(
|
|
239
|
+
await scoutCache.setHerdModules(freshSnapshot, cacheTtlMs, freshContentHash, userId);
|
|
234
240
|
}
|
|
235
241
|
catch (cacheError) {
|
|
236
242
|
console.warn("[useScoutRefresh] Cache save failed:", cacheError);
|
|
237
243
|
await recoverScoutCache(cacheError, "cache save", userId, async () => {
|
|
238
|
-
await scoutCache.setHerdModules(
|
|
244
|
+
await scoutCache.setHerdModules(freshSnapshot, cacheTtlMs, freshContentHash, userId);
|
|
239
245
|
});
|
|
240
246
|
}
|
|
241
247
|
}
|
|
@@ -248,7 +254,8 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
248
254
|
timestamp: Date.now(),
|
|
249
255
|
}));
|
|
250
256
|
dispatchInTransition(dispatch, setHerdModules({
|
|
251
|
-
modules:
|
|
257
|
+
modules: freshSnapshot.herd_modules,
|
|
258
|
+
productCatalog: freshSnapshot.product_catalog,
|
|
252
259
|
contentHash: freshContentHash,
|
|
253
260
|
}));
|
|
254
261
|
const dataProcessingDuration = Date.now() - dataProcessingStartTime;
|
|
@@ -259,7 +266,7 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
259
266
|
if (!isCurrent())
|
|
260
267
|
return;
|
|
261
268
|
console.error("Error refreshing scout data:", error);
|
|
262
|
-
if (
|
|
269
|
+
if (cachedSnapshot !== null) {
|
|
263
270
|
dispatch(setDataSource(EnumDataSource.CACHE));
|
|
264
271
|
complete(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED);
|
|
265
272
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,9 +6,12 @@ export * from "../helpers/bounding_boxes";
|
|
|
6
6
|
export * from "../helpers/email";
|
|
7
7
|
export * from "../helpers/eventMedia";
|
|
8
8
|
export * from "../helpers/eventUtils";
|
|
9
|
+
export * from "../helpers/galleryMedia";
|
|
9
10
|
export { default as get_gps_center } from "../helpers/gps";
|
|
10
11
|
export * from "../helpers/herd_modules_equal";
|
|
11
12
|
export * from "../helpers/location";
|
|
13
|
+
export * from "../helpers/mediaState";
|
|
14
|
+
export * from "../helpers/product_catalog";
|
|
12
15
|
export * from "../helpers/realtime_topic_access";
|
|
13
16
|
export * from "../helpers/storagePath";
|
|
14
17
|
export * from "../helpers/time";
|
|
@@ -28,6 +31,8 @@ export { server_create_document_template as create_document_template, server_get
|
|
|
28
31
|
export { server_search_embeddings_vertex_multimodal_001 as search_embeddings_vertex_multimodal_001 } from "../helpers/embeddings";
|
|
29
32
|
export { server_delete_event as delete_event, server_get_event_by_id as get_event_by_id, server_get_total_events_by_herd as get_total_events_by_herd, server_insert_event as insert_event, server_match_events_by_vertex_embedding as match_events_by_vertex_embedding, server_update_event as update_event, } from "../helpers/events";
|
|
30
33
|
export { server_create_battery_cycle_count as create_battery_cycle_count, server_delete_battery_cycle_count as delete_battery_cycle_count, server_get_battery_cycle_counts_by_battery_id as get_battery_cycle_counts_by_battery_id, server_update_battery_cycle_count as update_battery_cycle_count, } from "../helpers/battery_cycle_counts";
|
|
34
|
+
export { server_get_gallery_items_by_ids as get_gallery_items_by_ids } from "../helpers/gallery";
|
|
35
|
+
export type { IGalleryItemsByIdsArgs } from "../helpers/gallery.queries";
|
|
31
36
|
export { server_get_health_metrics as get_health_metrics, server_get_health_metrics_summary as get_health_metrics_summary, } from "../helpers/health_metrics";
|
|
32
37
|
export { server_get_herd_media_usage as get_herd_media_usage, server_get_latest_herd_media_usage as get_latest_herd_media_usage, } from "../helpers/herd_media_usage";
|
|
33
38
|
export { server_check_device_online_status as check_device_online_status, server_get_heartbeats_by_device as get_heartbeats_by_device, server_get_last_heartbeat_by_device as get_last_heartbeat_by_device, } from "../helpers/heartbeats";
|
package/dist/server/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";
|
|
@@ -28,6 +31,7 @@ export { server_create_document_template as create_document_template, server_get
|
|
|
28
31
|
export { server_search_embeddings_vertex_multimodal_001 as search_embeddings_vertex_multimodal_001 } from "../helpers/embeddings";
|
|
29
32
|
export { server_delete_event as delete_event, server_get_event_by_id as get_event_by_id, server_get_total_events_by_herd as get_total_events_by_herd, server_insert_event as insert_event, server_match_events_by_vertex_embedding as match_events_by_vertex_embedding, server_update_event as update_event, } from "../helpers/events";
|
|
30
33
|
export { server_create_battery_cycle_count as create_battery_cycle_count, server_delete_battery_cycle_count as delete_battery_cycle_count, server_get_battery_cycle_counts_by_battery_id as get_battery_cycle_counts_by_battery_id, server_update_battery_cycle_count as update_battery_cycle_count, } from "../helpers/battery_cycle_counts";
|
|
34
|
+
export { server_get_gallery_items_by_ids as get_gallery_items_by_ids } from "../helpers/gallery";
|
|
31
35
|
export { server_get_health_metrics as get_health_metrics, server_get_health_metrics_summary as get_health_metrics_summary, } from "../helpers/health_metrics";
|
|
32
36
|
export { server_get_herd_media_usage as get_herd_media_usage, server_get_latest_herd_media_usage as get_latest_herd_media_usage, } from "../helpers/herd_media_usage";
|
|
33
37
|
export { server_check_device_online_status as check_device_online_status, server_get_heartbeats_by_device as get_heartbeats_by_device, server_get_last_heartbeat_by_device as get_last_heartbeat_by_device, } from "../helpers/heartbeats";
|