@adventurelabs/scout-core 1.4.96 → 1.5.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.
Files changed (142) hide show
  1. package/dist/api_keys/actions.d.ts +1 -2
  2. package/dist/api_keys/actions.js +4 -83
  3. package/dist/api_keys/queries.d.ts +4 -0
  4. package/dist/api_keys/queries.js +52 -0
  5. package/dist/helpers/artifacts.d.ts +10 -15
  6. package/dist/helpers/artifacts.internal.d.ts +22 -0
  7. package/dist/helpers/artifacts.internal.js +151 -0
  8. package/dist/helpers/artifacts.js +48 -81
  9. package/dist/helpers/client_abilities_jwt_keys.d.ts +3 -4
  10. package/dist/helpers/client_abilities_jwt_keys.js +7 -7
  11. package/dist/helpers/client_abilities_jwt_keys_internal.d.ts +9 -0
  12. package/dist/helpers/client_abilities_jwt_keys_internal.js +20 -0
  13. package/dist/helpers/client_abilities_token_server.d.ts +1 -5
  14. package/dist/helpers/client_abilities_token_server.js +2 -2
  15. package/dist/helpers/connectivity.d.ts +2 -3
  16. package/dist/helpers/connectivity.internal.d.ts +10 -0
  17. package/dist/helpers/connectivity.internal.js +47 -0
  18. package/dist/helpers/connectivity.js +6 -7
  19. package/dist/helpers/devices.d.ts +1 -3
  20. package/dist/helpers/devices.js +3 -12
  21. package/dist/helpers/devices.queries.d.ts +5 -0
  22. package/dist/helpers/devices.queries.js +10 -0
  23. package/dist/helpers/documents.d.ts +1 -0
  24. package/dist/helpers/documents.js +11 -2
  25. package/dist/helpers/embeddings.d.ts +1 -3
  26. package/dist/helpers/embeddings.internal.d.ts +5 -0
  27. package/dist/helpers/embeddings.internal.js +14 -0
  28. package/dist/helpers/embeddings.js +2 -2
  29. package/dist/helpers/events.d.ts +4 -6
  30. package/dist/helpers/events.internal.d.ts +13 -0
  31. package/dist/helpers/events.internal.js +88 -0
  32. package/dist/helpers/events.js +16 -17
  33. package/dist/helpers/herd_allowed_domains.d.ts +1 -3
  34. package/dist/helpers/herd_allowed_domains.js +2 -2
  35. package/dist/helpers/herd_allowed_domains_internal.d.ts +5 -0
  36. package/dist/helpers/herd_allowed_domains_internal.js +12 -0
  37. package/dist/helpers/herd_modules.queries.d.ts +3 -0
  38. package/dist/helpers/herd_modules.queries.js +106 -0
  39. package/dist/helpers/herd_roles.d.ts +12 -14
  40. package/dist/helpers/herd_roles.js +26 -34
  41. package/dist/helpers/herd_roles_internal.d.ts +18 -0
  42. package/dist/helpers/herd_roles_internal.js +159 -0
  43. package/dist/helpers/herd_scoped.queries.d.ts +7 -0
  44. package/dist/helpers/herd_scoped.queries.js +45 -0
  45. package/dist/helpers/herds.d.ts +2 -4
  46. package/dist/helpers/herds.js +11 -142
  47. package/dist/helpers/herds.queries.d.ts +5 -0
  48. package/dist/helpers/herds.queries.js +12 -0
  49. package/dist/helpers/index.d.ts +1 -0
  50. package/dist/helpers/index.js +1 -0
  51. package/dist/helpers/invitations.d.ts +4 -6
  52. package/dist/helpers/invitations.js +8 -8
  53. package/dist/helpers/invitations_internal.d.ts +10 -0
  54. package/dist/helpers/invitations_internal.js +63 -0
  55. package/dist/helpers/layers.d.ts +1 -3
  56. package/dist/helpers/layers.js +3 -18
  57. package/dist/helpers/layers.queries.d.ts +5 -0
  58. package/dist/helpers/layers.queries.js +20 -0
  59. package/dist/helpers/operating_contexts.d.ts +43 -0
  60. package/dist/helpers/operating_contexts.js +222 -0
  61. package/dist/helpers/plans.d.ts +1 -3
  62. package/dist/helpers/plans.js +3 -18
  63. package/dist/helpers/plans.queries.d.ts +5 -0
  64. package/dist/helpers/plans.queries.js +20 -0
  65. package/dist/helpers/providers.d.ts +1 -3
  66. package/dist/helpers/providers.js +3 -18
  67. package/dist/helpers/providers.queries.d.ts +5 -0
  68. package/dist/helpers/providers.queries.js +20 -0
  69. package/dist/helpers/pubsub_token_server.d.ts +1 -5
  70. package/dist/helpers/pubsub_token_server.js +2 -2
  71. package/dist/helpers/realtime_topic_access.d.ts +5 -0
  72. package/dist/helpers/realtime_topic_access.js +55 -0
  73. package/dist/helpers/session_summaries.d.ts +3 -4
  74. package/dist/helpers/session_summaries.js +13 -43
  75. package/dist/helpers/session_summaries.queries.d.ts +14 -0
  76. package/dist/helpers/session_summaries.queries.js +19 -0
  77. package/dist/helpers/sessions.d.ts +7 -8
  78. package/dist/helpers/sessions.js +23 -58
  79. package/dist/helpers/sessions.queries.d.ts +4 -0
  80. package/dist/helpers/sessions.queries.js +22 -0
  81. package/dist/helpers/storage.d.ts +2 -4
  82. package/dist/helpers/storage.js +7 -54
  83. package/dist/helpers/storage_internal.d.ts +4 -0
  84. package/dist/helpers/storage_internal.js +29 -0
  85. package/dist/helpers/tags.js +5 -5
  86. package/dist/helpers/users.d.ts +7 -8
  87. package/dist/helpers/users.js +13 -41
  88. package/dist/helpers/users.queries.d.ts +5 -0
  89. package/dist/helpers/users.queries.js +30 -0
  90. package/dist/helpers/versions_software_internal.d.ts +6 -0
  91. package/dist/helpers/versions_software_internal.js +53 -0
  92. package/dist/helpers/versions_software_server.d.ts +2 -3
  93. package/dist/helpers/versions_software_server.js +20 -18
  94. package/dist/hooks/index.d.ts +2 -1
  95. package/dist/hooks/index.js +2 -1
  96. package/dist/hooks/useAuthorizedRealtimeTopics.d.ts +1 -0
  97. package/dist/hooks/useAuthorizedRealtimeTopics.js +14 -0
  98. package/dist/hooks/useScoutRealtimeAnalysisJobs.d.ts +2 -1
  99. package/dist/hooks/useScoutRealtimeAnalysisJobs.js +3 -5
  100. package/dist/hooks/useScoutRealtimeAnalysisTasks.d.ts +2 -1
  101. package/dist/hooks/useScoutRealtimeAnalysisTasks.js +3 -5
  102. package/dist/hooks/useScoutRealtimeBroadcast.d.ts +9 -5
  103. package/dist/hooks/useScoutRealtimeBroadcast.js +118 -25
  104. package/dist/hooks/useScoutRealtimeConnectivity.d.ts +2 -1
  105. package/dist/hooks/useScoutRealtimeConnectivity.js +18 -76
  106. package/dist/hooks/useScoutRealtimeContacts.d.ts +2 -1
  107. package/dist/hooks/useScoutRealtimeContacts.js +3 -3
  108. package/dist/hooks/useScoutRealtimeDevices.d.ts +2 -1
  109. package/dist/hooks/useScoutRealtimeDevices.js +3 -64
  110. package/dist/hooks/useScoutRealtimeDocuments.d.ts +3 -3
  111. package/dist/hooks/useScoutRealtimeDocuments.js +7 -5
  112. package/dist/hooks/useScoutRealtimeEvents.d.ts +2 -1
  113. package/dist/hooks/useScoutRealtimeEvents.js +3 -69
  114. package/dist/hooks/useScoutRealtimeMaintenanceRequests.d.ts +2 -1
  115. package/dist/hooks/useScoutRealtimeMaintenanceRequests.js +3 -3
  116. package/dist/hooks/useScoutRealtimeOperatingContexts.d.ts +12 -0
  117. package/dist/hooks/useScoutRealtimeOperatingContexts.js +26 -0
  118. package/dist/hooks/useScoutRealtimeParts.d.ts +2 -1
  119. package/dist/hooks/useScoutRealtimeParts.js +3 -3
  120. package/dist/hooks/useScoutRealtimePins.d.ts +2 -1
  121. package/dist/hooks/useScoutRealtimePins.js +3 -69
  122. package/dist/hooks/useScoutRealtimePlans.d.ts +2 -1
  123. package/dist/hooks/useScoutRealtimePlans.js +3 -3
  124. package/dist/hooks/useScoutRealtimeSessionIncidents.d.ts +2 -1
  125. package/dist/hooks/useScoutRealtimeSessionIncidents.js +3 -3
  126. package/dist/hooks/useScoutRealtimeSessions.d.ts +2 -1
  127. package/dist/hooks/useScoutRealtimeSessions.js +3 -69
  128. package/dist/hooks/useScoutRealtimeTags.d.ts +2 -1
  129. package/dist/hooks/useScoutRealtimeTags.js +3 -69
  130. package/dist/hooks/useScoutRealtimeVersionsSoftware.d.ts +2 -1
  131. package/dist/hooks/useScoutRealtimeVersionsSoftware.js +3 -5
  132. package/dist/index.d.ts +2 -0
  133. package/dist/index.js +2 -0
  134. package/dist/providers/ScoutRefreshProvider.d.ts +1 -0
  135. package/dist/providers/ScoutRefreshProvider.js +3 -0
  136. package/dist/supabase/server.d.ts +4 -1
  137. package/dist/supabase/server.js +6 -5
  138. package/dist/types/db.d.ts +82 -0
  139. package/dist/types/realtime.d.ts +1 -0
  140. package/dist/types/supabase.d.ts +952 -100
  141. package/dist/types/supabase.js +26 -1
  142. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  export { useScoutRefresh, type UseScoutRefreshOptions, } from "./useScoutRefresh";
2
- export { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
2
+ export { useScoutRealtimeBroadcast, type ScoutRealtimeBroadcastOptions, type ScoutRealtimeAuthErrorMode, } from "./useScoutRealtimeBroadcast";
3
3
  export { useScoutRealtimeConnectivity } from "./useScoutRealtimeConnectivity";
4
4
  export { useScoutRealtimeDevices } from "./useScoutRealtimeDevices";
5
5
  export { useScoutRealtimeVersionsSoftware } from "./useScoutRealtimeVersionsSoftware";
@@ -15,5 +15,6 @@ export { useScoutRealtimeDocuments, type DocumentRealtimeChannel, } from "./useS
15
15
  export { useScoutRealtimeMaintenanceRequests } from "./useScoutRealtimeMaintenanceRequests";
16
16
  export { useScoutRealtimeContacts } from "./useScoutRealtimeContacts";
17
17
  export { useScoutRealtimeSessionIncidents } from "./useScoutRealtimeSessionIncidents";
18
+ export { useScoutRealtimeOperatingContexts, type OperatingContextRealtimeData, type OperatingContextRealtimeRow, } from "./useScoutRealtimeOperatingContexts";
18
19
  export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useInfiniteFeedByHerd, useInfiniteFeedByDevice, useInfiniteAnalysisJobs, useInfiniteAnalysisTasks, useIntersectionObserver, type UseInfiniteScrollOptions, type UseAnalysisJobsInfiniteOptions, type UseAnalysisTasksInfiniteOptions, } from "./useInfiniteQuery";
19
20
  export { useLoadingPerformance, useSessionSummariesByHerd, useHasSessionSummaries, } from "../store/hooks";
@@ -1,5 +1,5 @@
1
1
  export { useScoutRefresh, } from "./useScoutRefresh";
2
- export { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
2
+ export { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
3
3
  export { useScoutRealtimeConnectivity } from "./useScoutRealtimeConnectivity";
4
4
  export { useScoutRealtimeDevices } from "./useScoutRealtimeDevices";
5
5
  export { useScoutRealtimeVersionsSoftware } from "./useScoutRealtimeVersionsSoftware";
@@ -15,6 +15,7 @@ export { useScoutRealtimeDocuments, } from "./useScoutRealtimeDocuments";
15
15
  export { useScoutRealtimeMaintenanceRequests } from "./useScoutRealtimeMaintenanceRequests";
16
16
  export { useScoutRealtimeContacts } from "./useScoutRealtimeContacts";
17
17
  export { useScoutRealtimeSessionIncidents } from "./useScoutRealtimeSessionIncidents";
18
+ export { useScoutRealtimeOperatingContexts, } from "./useScoutRealtimeOperatingContexts";
18
19
  // RTK Query infinite scroll hooks
19
20
  export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useInfiniteFeedByHerd, useInfiniteFeedByDevice, useInfiniteAnalysisJobs, useInfiniteAnalysisTasks, useIntersectionObserver, } from "./useInfiniteQuery";
20
21
  // Session summaries and performance hooks
@@ -0,0 +1 @@
1
+ export declare function useAuthorizedRealtimeTopics(topics: string[]): string[];
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ import { filter_realtime_topics } from "../helpers/realtime_topic_access";
4
+ import { useOptionalClientAbilitiesMint } from "../providers/ScoutRefreshProvider";
5
+ export function useAuthorizedRealtimeTopics(topics) {
6
+ const abilities = useOptionalClientAbilitiesMint();
7
+ const topicsKey = topics.join("|");
8
+ return useMemo(() => {
9
+ const requested = topicsKey.split("|").filter(Boolean);
10
+ if (!abilities)
11
+ return requested;
12
+ return filter_realtime_topics(abilities.status, abilities.mint, requested);
13
+ }, [abilities, topicsKey]);
14
+ }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IAnalysisJob } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeAnalysisJobs(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IAnalysisJob> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeAnalysisJobs(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IAnalysisJob> | null, () => void];
@@ -1,7 +1,5 @@
1
1
  "use client";
2
- import { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
3
- export function useScoutRealtimeAnalysisJobs(scoutSupabase) {
4
- return useScoutRealtimeBroadcast(scoutSupabase, [
5
- "analysis_jobs_changes",
6
- ]);
2
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
3
+ export function useScoutRealtimeAnalysisJobs(scoutSupabase, options) {
4
+ return useScoutRealtimeBroadcast(scoutSupabase, ["analysis_jobs_changes"], options);
7
5
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IAnalysisTask } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeAnalysisTasks(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IAnalysisTask> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeAnalysisTasks(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IAnalysisTask> | null, () => void];
@@ -1,7 +1,5 @@
1
1
  "use client";
2
- import { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
3
- export function useScoutRealtimeAnalysisTasks(scoutSupabase) {
4
- return useScoutRealtimeBroadcast(scoutSupabase, [
5
- "analysis_tasks_changes",
6
- ]);
2
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
3
+ export function useScoutRealtimeAnalysisTasks(scoutSupabase, options) {
4
+ return useScoutRealtimeBroadcast(scoutSupabase, ["analysis_tasks_changes"], options);
7
5
  }
@@ -1,8 +1,12 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { RealtimeData } from "../types/realtime";
4
- /**
5
- * Subscribes to private realtime topics and reports the most recent row change across them.
6
- * Changing the topic set resubscribes and clears the pending change.
7
- */
8
- export declare function useScoutRealtimeBroadcast<T>(scoutSupabase: SupabaseClient<Database>, topics: string[]): [RealtimeData<T> | null, () => void];
4
+ export type ScoutRealtimeAuthErrorMode = "stop" | "backoff";
5
+ export type ScoutRealtimeBroadcastOptions = {
6
+ /** `stop` prevents Phoenix rejoin loops on auth failure. */
7
+ authErrorRetry?: ScoutRealtimeAuthErrorMode;
8
+ backoffInitialMs?: number;
9
+ backoffMaxMs?: number;
10
+ backoffMaxAttempts?: number;
11
+ };
12
+ export declare function useScoutRealtimeBroadcast<T>(scoutSupabase: SupabaseClient<Database>, topics: string[], options?: ScoutRealtimeBroadcastOptions, rowFilter?: (row: T) => boolean): [RealtimeData<T> | null, () => void];
@@ -1,44 +1,137 @@
1
1
  "use client";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useState } from "react";
3
3
  import { EnumRealtimeOperation } from "../types/realtime";
4
+ import { useAuthorizedRealtimeTopics } from "./useAuthorizedRealtimeTopics";
4
5
  const OPERATIONS = {
5
6
  INSERT: EnumRealtimeOperation.INSERT,
6
7
  UPDATE: EnumRealtimeOperation.UPDATE,
7
8
  DELETE: EnumRealtimeOperation.DELETE,
8
9
  };
9
- /**
10
- * Subscribes to private realtime topics and reports the most recent row change across them.
11
- * Changing the topic set resubscribes and clears the pending change.
12
- */
13
- export function useScoutRealtimeBroadcast(scoutSupabase, topics) {
10
+ const DEFAULT_BACKOFF_INITIAL_MS = 1000;
11
+ const DEFAULT_BACKOFF_MAX_MS = 60000;
12
+ function isRealtimeAuthError(error) {
13
+ const message = error?.message ?? "";
14
+ return /unauthorized|permission/i.test(message);
15
+ }
16
+ function backoffDelayMs(attempt, initialMs, maxMs) {
17
+ return Math.min(maxMs, initialMs * 2 ** Math.max(0, attempt));
18
+ }
19
+ export function useScoutRealtimeBroadcast(scoutSupabase, topics, options = {}, rowFilter) {
20
+ const { authErrorRetry = "stop", backoffInitialMs = DEFAULT_BACKOFF_INITIAL_MS, backoffMaxMs = DEFAULT_BACKOFF_MAX_MS, backoffMaxAttempts = Number.POSITIVE_INFINITY, } = options;
14
21
  const [latestUpdate, setLatestUpdate] = useState(null);
15
- // Key the subscription on the topic set rather than the array identity, so callers can build
16
- // their topics inline.
17
- const topicsKey = topics.join("|");
18
- const subscribedTopics = useMemo(() => topicsKey.split("|").filter(Boolean), [topicsKey]);
22
+ const subscribedTopics = useAuthorizedRealtimeTopics(topics);
19
23
  const clearLatestUpdate = useCallback(() => setLatestUpdate(null), []);
20
24
  useEffect(() => {
21
25
  setLatestUpdate(null);
22
26
  if (!scoutSupabase || subscribedTopics.length === 0)
23
27
  return;
24
- const channels = subscribedTopics.map((topic) => scoutSupabase
25
- .channel(topic, { config: { private: true } })
26
- .on("broadcast", { event: "*" }, (message) => {
27
- const change = message.payload;
28
- const operation = OPERATIONS[change.operation];
29
- const row = change.record ?? change.old_record;
30
- if (operation === undefined || !row)
28
+ let cancelled = false;
29
+ const channels = new Map();
30
+ const authAttempts = new Map();
31
+ const blockedTopics = new Set();
32
+ const retryTimers = new Map();
33
+ const removeTopicChannel = (topic) => {
34
+ const channel = channels.get(topic);
35
+ if (!channel)
36
+ return;
37
+ channels.delete(topic);
38
+ void scoutSupabase.removeChannel(channel);
39
+ };
40
+ const scheduleRetry = (topic) => {
41
+ if (authErrorRetry !== "backoff" || cancelled)
42
+ return;
43
+ const attempt = authAttempts.get(topic) ?? 0;
44
+ if (attempt >= backoffMaxAttempts)
31
45
  return;
32
- setLatestUpdate({ data: row, operation });
33
- })
34
- .subscribe((status) => {
35
- if (status === "CHANNEL_ERROR") {
36
- console.warn(`[scout-core realtime] 🟡 ${topic} unavailable`);
46
+ authAttempts.set(topic, attempt + 1);
47
+ const existing = retryTimers.get(topic);
48
+ if (existing)
49
+ clearTimeout(existing);
50
+ retryTimers.set(topic, setTimeout(() => {
51
+ retryTimers.delete(topic);
52
+ void authenticateAndJoin([topic]);
53
+ }, backoffDelayMs(attempt, backoffInitialMs, backoffMaxMs)));
54
+ };
55
+ const joinTopic = (topic) => {
56
+ if (cancelled || channels.has(topic))
57
+ return;
58
+ const channel = scoutSupabase
59
+ .channel(topic, { config: { private: true } })
60
+ .on("broadcast", { event: "*" }, (message) => {
61
+ const change = message.payload;
62
+ const operation = OPERATIONS[change.operation];
63
+ const row = change.record ?? change.old_record;
64
+ if (operation === undefined || !row)
65
+ return;
66
+ if (rowFilter && !rowFilter(row))
67
+ return;
68
+ setLatestUpdate({ data: row, operation, table: change.table });
69
+ });
70
+ channels.set(topic, channel);
71
+ channel.subscribe((status, error) => {
72
+ if (status === "SUBSCRIBED") {
73
+ authAttempts.delete(topic);
74
+ blockedTopics.delete(topic);
75
+ return;
76
+ }
77
+ if (status !== "CHANNEL_ERROR")
78
+ return;
79
+ console.warn(`[scout-core realtime] 🟡 ${topic} unavailable`, error?.message ?? "");
80
+ if (!isRealtimeAuthError(error) || cancelled)
81
+ return;
82
+ blockedTopics.add(topic);
83
+ removeTopicChannel(topic);
84
+ scheduleRetry(topic);
85
+ });
86
+ };
87
+ async function authenticateAndJoin(topicsToJoin) {
88
+ try {
89
+ await scoutSupabase.realtime.setAuth();
90
+ }
91
+ catch (error) {
92
+ console.warn("[scout-core realtime] failed to refresh auth", error);
93
+ }
94
+ if (!cancelled)
95
+ topicsToJoin.forEach(joinTopic);
96
+ }
97
+ const retryBlockedTopics = () => {
98
+ const topicsToRetry = Array.from(blockedTopics);
99
+ blockedTopics.forEach((topic) => {
100
+ const timer = retryTimers.get(topic);
101
+ if (timer)
102
+ clearTimeout(timer);
103
+ retryTimers.delete(topic);
104
+ authAttempts.delete(topic);
105
+ });
106
+ if (topicsToRetry.length > 0)
107
+ void authenticateAndJoin(topicsToRetry);
108
+ };
109
+ const { data: { subscription: authSubscription }, } = scoutSupabase.auth.onAuthStateChange((event) => {
110
+ if (event === "INITIAL_SESSION" ||
111
+ event === "SIGNED_IN" ||
112
+ event === "TOKEN_REFRESHED") {
113
+ retryBlockedTopics();
37
114
  }
38
- }));
115
+ });
116
+ void authenticateAndJoin(subscribedTopics);
39
117
  return () => {
40
- channels.forEach((channel) => scoutSupabase.removeChannel(channel));
118
+ cancelled = true;
119
+ authSubscription.unsubscribe();
120
+ retryTimers.forEach((timer) => clearTimeout(timer));
121
+ retryTimers.clear();
122
+ channels.forEach((channel) => {
123
+ void scoutSupabase.removeChannel(channel);
124
+ });
125
+ channels.clear();
41
126
  };
42
- }, [scoutSupabase, subscribedTopics]);
127
+ }, [
128
+ scoutSupabase,
129
+ subscribedTopics,
130
+ authErrorRetry,
131
+ backoffInitialMs,
132
+ backoffMaxMs,
133
+ backoffMaxAttempts,
134
+ rowFilter,
135
+ ]);
43
136
  return [latestUpdate, clearLatestUpdate];
44
137
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IConnectivityWithCoordinates } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeConnectivity(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IConnectivityWithCoordinates> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeConnectivity(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IConnectivityWithCoordinates> | null, () => void];
@@ -1,84 +1,26 @@
1
1
  "use client";
2
2
  import { useSelector } from "react-redux";
3
- import { useEffect, useRef, useCallback, useState, useMemo } from "react";
4
- import { EnumRealtimeOperation } from "../types/realtime";
5
- export function useScoutRealtimeConnectivity(scoutSupabase) {
6
- const channels = useRef([]);
7
- const [latestConnectivityUpdate, setLatestConnectivityUpdate] = useState(null);
3
+ import { useMemo } from "react";
4
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
5
+ const GPS_DEVICE_TYPES = [
6
+ "gps_tracker",
7
+ "gps_tracker_vehicle",
8
+ "gps_tracker_person",
9
+ ];
10
+ function isGpsConnectivity(row) {
11
+ return Boolean(row.device_id && !row.session_id);
12
+ }
13
+ export function useScoutRealtimeConnectivity(scoutSupabase, options) {
8
14
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
9
15
  const herdModules = useSelector((state) => state.scout.herd_modules);
10
- // Create stable reference for GPS device IDs to prevent unnecessary refetching
11
- const gpsDeviceIds = useMemo(() => {
16
+ const hasGpsDevices = useMemo(() => {
12
17
  if (!activeHerdId)
13
- return "";
18
+ return false;
14
19
  const activeHerdModule = herdModules.find((hm) => hm.herd.id?.toString() === activeHerdId);
15
- if (!activeHerdModule)
16
- return "";
17
- const gpsDevices = activeHerdModule.devices.filter((device) => device.device_type &&
18
- ["gps_tracker", "gps_tracker_vehicle", "gps_tracker_person"].includes(device.device_type));
19
- return gpsDevices
20
- .map((d) => d.id)
21
- .filter(Boolean)
22
- .sort()
23
- .join(",");
20
+ return activeHerdModule?.devices.some((device) => device.device_type &&
21
+ GPS_DEVICE_TYPES.includes(device.device_type)) ?? false;
24
22
  }, [activeHerdId, herdModules]);
25
- // Handle connectivity broadcasts
26
- const handleConnectivityBroadcast = useCallback((payload) => {
27
- const { event, payload: data } = payload;
28
- const connectivityData = data.record || data.old_record;
29
- // Only process GPS tracker data (no session_id)
30
- if (!connectivityData?.device_id || connectivityData.session_id) {
31
- return;
32
- }
33
- let operation;
34
- switch (data.operation) {
35
- case "INSERT":
36
- operation = EnumRealtimeOperation.INSERT;
37
- break;
38
- case "UPDATE":
39
- operation = EnumRealtimeOperation.UPDATE;
40
- break;
41
- case "DELETE":
42
- operation = EnumRealtimeOperation.DELETE;
43
- break;
44
- default:
45
- return;
46
- }
47
- console.log(`[scout-core realtime] CONNECTIVITY ${data.operation} received for device ${connectivityData.device_id}:`, JSON.stringify(connectivityData));
48
- const realtimeData = {
49
- data: connectivityData,
50
- operation,
51
- };
52
- setLatestConnectivityUpdate(realtimeData);
53
- }, []);
54
- // Clear latest update
55
- const clearLatestUpdate = useCallback(() => {
56
- setLatestConnectivityUpdate(null);
57
- }, []);
58
- useEffect(() => {
59
- if (!scoutSupabase || gpsDeviceIds === "")
60
- return;
61
- // Clean up existing channels
62
- channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
63
- channels.current = [];
64
- // Clear previous update when switching herds
65
- clearLatestUpdate();
66
- // Create connectivity channel
67
- const channel = scoutSupabase
68
- .channel(`${activeHerdId}-connectivity`, { config: { private: true } })
69
- .on("broadcast", { event: "*" }, handleConnectivityBroadcast)
70
- .subscribe();
71
- channels.current.push(channel);
72
- return () => {
73
- channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
74
- channels.current = [];
75
- };
76
- }, [
77
- scoutSupabase,
78
- gpsDeviceIds,
79
- activeHerdId,
80
- handleConnectivityBroadcast,
81
- clearLatestUpdate,
82
- ]);
83
- return [latestConnectivityUpdate, clearLatestUpdate];
23
+ return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId && hasGpsDevices
24
+ ? [`${activeHerdId}-connectivity`]
25
+ : [], options, isGpsConnectivity);
84
26
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IContact } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeContacts(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IContact> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeContacts(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IContact> | null, () => void];
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { useSelector } from "react-redux";
3
- import { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
4
- export function useScoutRealtimeContacts(scoutSupabase) {
3
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
4
+ export function useScoutRealtimeContacts(scoutSupabase, options) {
5
5
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
6
- return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-contacts`] : []);
6
+ return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-contacts`] : [], options);
7
7
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IDevicePrettyLocation } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeDevices(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IDevicePrettyLocation> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeDevices(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IDevicePrettyLocation> | null, () => void];
@@ -1,68 +1,7 @@
1
1
  "use client";
2
2
  import { useSelector } from "react-redux";
3
- import { useEffect, useRef, useCallback, useState } from "react";
4
- import { EnumRealtimeOperation } from "../types/realtime";
5
- export function useScoutRealtimeDevices(scoutSupabase) {
6
- const channels = useRef([]);
7
- const [latestDeviceUpdate, setLatestDeviceUpdate] = useState(null);
3
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
4
+ export function useScoutRealtimeDevices(scoutSupabase, options) {
8
5
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
9
- const handleDeviceBroadcast = useCallback((message) => {
10
- const { payload: data } = message;
11
- if (data.table !== "devices" || data.schema !== "public") {
12
- return;
13
- }
14
- const deviceData = data.operation === "DELETE"
15
- ? data.old_record
16
- : data.record ?? data.old_record;
17
- if (!deviceData)
18
- return;
19
- let operation;
20
- switch (data.operation) {
21
- case "INSERT":
22
- operation = EnumRealtimeOperation.INSERT;
23
- break;
24
- case "UPDATE":
25
- operation = EnumRealtimeOperation.UPDATE;
26
- break;
27
- case "DELETE":
28
- operation = EnumRealtimeOperation.DELETE;
29
- break;
30
- default:
31
- return;
32
- }
33
- const realtimeData = {
34
- data: deviceData,
35
- operation,
36
- };
37
- setLatestDeviceUpdate(realtimeData);
38
- }, []);
39
- const clearLatestUpdate = useCallback(() => {
40
- setLatestDeviceUpdate(null);
41
- }, []);
42
- useEffect(() => {
43
- if (!scoutSupabase)
44
- return;
45
- channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
46
- channels.current = [];
47
- clearLatestUpdate();
48
- if (activeHerdId) {
49
- const channel = scoutSupabase
50
- .channel(`${activeHerdId}-devices`, { config: { private: true } })
51
- .on("broadcast", { event: "*" }, handleDeviceBroadcast)
52
- .subscribe((status) => {
53
- if (status === "SUBSCRIBED") {
54
- console.log(`[Devices] ✅ Connected to herd ${activeHerdId}`);
55
- }
56
- else if (status === "CHANNEL_ERROR") {
57
- console.warn(`[Devices] 🟡 Failed to connect to herd ${activeHerdId}`);
58
- }
59
- });
60
- channels.current.push(channel);
61
- }
62
- return () => {
63
- channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
64
- channels.current = [];
65
- };
66
- }, [scoutSupabase, activeHerdId, handleDeviceBroadcast, clearLatestUpdate]);
67
- return [latestDeviceUpdate, clearLatestUpdate];
6
+ return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-devices`] : [], options);
68
7
  }
@@ -2,6 +2,6 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IDocument } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- /** One channel per document owner class, each gated on that class's read ability. */
6
- export type DocumentRealtimeChannel = "herd" | "device" | "user" | "own" | "product";
7
- export declare function useScoutRealtimeDocuments(scoutSupabase: SupabaseClient<Database>, channels?: DocumentRealtimeChannel[]): [RealtimeData<IDocument> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export type DocumentRealtimeChannel = "herd" | "device" | "user" | "own" | "product" | "operating_context";
7
+ export declare function useScoutRealtimeDocuments(scoutSupabase: SupabaseClient<Database>, channels?: DocumentRealtimeChannel[], options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IDocument> | null, () => void];
@@ -1,22 +1,24 @@
1
1
  "use client";
2
+ import { useMemo } from "react";
2
3
  import { useSelector } from "react-redux";
3
- import { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
4
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
4
5
  const ALL_DOCUMENT_CHANNELS = [
5
6
  "herd",
6
7
  "device",
7
8
  "user",
8
9
  "own",
9
10
  "product",
11
+ "operating_context",
10
12
  ];
11
- export function useScoutRealtimeDocuments(scoutSupabase, channels = ALL_DOCUMENT_CHANNELS) {
13
+ export function useScoutRealtimeDocuments(scoutSupabase, channels = ALL_DOCUMENT_CHANNELS, options) {
12
14
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
13
15
  const userId = useSelector((state) => state.scout.user?.id ?? null);
14
- const topics = channels.flatMap((channel) => {
16
+ const topics = useMemo(() => channels.flatMap((channel) => {
15
17
  if (channel === "product")
16
18
  return ["product_documents_changes"];
17
19
  if (channel === "own")
18
20
  return userId ? [`user_${userId}-documents`] : [];
19
21
  return activeHerdId ? [`${activeHerdId}-${channel}_documents`] : [];
20
- });
21
- return useScoutRealtimeBroadcast(scoutSupabase, topics);
22
+ }), [channels, activeHerdId, userId]);
23
+ return useScoutRealtimeBroadcast(scoutSupabase, topics, options);
22
24
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IEventAndTagsPrettyLocation } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeEvents(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IEventAndTagsPrettyLocation> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeEvents(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IEventAndTagsPrettyLocation> | null, () => void];
@@ -1,73 +1,7 @@
1
1
  "use client";
2
2
  import { useSelector } from "react-redux";
3
- import { useEffect, useRef, useCallback, useState } from "react";
4
- import { EnumRealtimeOperation } from "../types/realtime";
5
- export function useScoutRealtimeEvents(scoutSupabase) {
6
- const channels = useRef([]);
7
- const [latestEventUpdate, setLatestEventUpdate] = useState(null);
3
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
4
+ export function useScoutRealtimeEvents(scoutSupabase, options) {
8
5
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
9
- // Event broadcast handler - just pass data, don't mutate state
10
- const handleEventBroadcast = useCallback((payload) => {
11
- console.log("[Events] Broadcast received:", payload.payload.operation);
12
- const data = payload.payload;
13
- const eventData = data.record || data.old_record;
14
- if (!eventData)
15
- return;
16
- let operation;
17
- switch (data.operation) {
18
- case "INSERT":
19
- operation = EnumRealtimeOperation.INSERT;
20
- console.log("[Events] New event received:", data.record);
21
- break;
22
- case "UPDATE":
23
- operation = EnumRealtimeOperation.UPDATE;
24
- console.log("[Events] Event updated:", data.record);
25
- break;
26
- case "DELETE":
27
- operation = EnumRealtimeOperation.DELETE;
28
- console.log("[Events] Event deleted:", data.old_record);
29
- break;
30
- default:
31
- return;
32
- }
33
- const realtimeData = {
34
- data: eventData,
35
- operation,
36
- };
37
- console.log(`[scout-core realtime] EVENT ${data.operation} received:`, JSON.stringify(realtimeData));
38
- setLatestEventUpdate(realtimeData);
39
- }, []);
40
- // Clear latest update
41
- const clearLatestUpdate = useCallback(() => {
42
- setLatestEventUpdate(null);
43
- }, []);
44
- const cleanupChannels = () => {
45
- channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
46
- channels.current = [];
47
- };
48
- const createEventsChannel = (herdId) => {
49
- return scoutSupabase
50
- .channel(`${herdId}-events`, { config: { private: true } })
51
- .on("broadcast", { event: "*" }, handleEventBroadcast)
52
- .subscribe((status) => {
53
- if (status === "SUBSCRIBED") {
54
- console.log(`[Events] ✅ Connected to herd ${herdId}`);
55
- }
56
- else if (status === "CHANNEL_ERROR") {
57
- console.warn(`[Events] 🟡 Failed to connect to herd ${herdId}`);
58
- }
59
- });
60
- };
61
- useEffect(() => {
62
- cleanupChannels();
63
- // Clear previous update when switching herds
64
- clearLatestUpdate();
65
- // Create events channel for active herd
66
- if (activeHerdId) {
67
- const channel = createEventsChannel(activeHerdId);
68
- channels.current.push(channel);
69
- }
70
- return cleanupChannels;
71
- }, [activeHerdId, clearLatestUpdate]);
72
- return [latestEventUpdate, clearLatestUpdate];
6
+ return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-events`] : [], options);
73
7
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IMaintenanceRequest } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeMaintenanceRequests(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IMaintenanceRequest> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeMaintenanceRequests(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IMaintenanceRequest> | null, () => void];
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { useSelector } from "react-redux";
3
- import { useScoutRealtimeBroadcast } from "./useScoutRealtimeBroadcast";
4
- export function useScoutRealtimeMaintenanceRequests(scoutSupabase) {
3
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
4
+ export function useScoutRealtimeMaintenanceRequests(scoutSupabase, options) {
5
5
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
6
- return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-maintenance_requests`] : []);
6
+ return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-maintenance_requests`] : [], options);
7
7
  }