@adventurelabs/scout-core 1.5.0 → 1.5.2

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 (59) hide show
  1. package/dist/helpers/artifactMedia.d.ts +5 -0
  2. package/dist/helpers/artifactMedia.js +31 -0
  3. package/dist/helpers/artifacts.js +13 -19
  4. package/dist/helpers/documents.d.ts +1 -0
  5. package/dist/helpers/documents.js +11 -2
  6. package/dist/helpers/index.d.ts +1 -0
  7. package/dist/helpers/index.js +1 -0
  8. package/dist/helpers/operating_contexts.d.ts +43 -0
  9. package/dist/helpers/operating_contexts.js +222 -0
  10. package/dist/helpers/realtime_topic_access.d.ts +5 -0
  11. package/dist/helpers/realtime_topic_access.js +55 -0
  12. package/dist/hooks/index.d.ts +2 -1
  13. package/dist/hooks/index.js +2 -1
  14. package/dist/hooks/useAuthorizedRealtimeTopics.d.ts +1 -0
  15. package/dist/hooks/useAuthorizedRealtimeTopics.js +14 -0
  16. package/dist/hooks/useScoutRealtimeAnalysisJobs.d.ts +2 -1
  17. package/dist/hooks/useScoutRealtimeAnalysisJobs.js +3 -5
  18. package/dist/hooks/useScoutRealtimeAnalysisTasks.d.ts +2 -1
  19. package/dist/hooks/useScoutRealtimeAnalysisTasks.js +3 -5
  20. package/dist/hooks/useScoutRealtimeBroadcast.d.ts +9 -5
  21. package/dist/hooks/useScoutRealtimeBroadcast.js +118 -25
  22. package/dist/hooks/useScoutRealtimeConnectivity.d.ts +2 -1
  23. package/dist/hooks/useScoutRealtimeConnectivity.js +18 -76
  24. package/dist/hooks/useScoutRealtimeContacts.d.ts +2 -1
  25. package/dist/hooks/useScoutRealtimeContacts.js +3 -3
  26. package/dist/hooks/useScoutRealtimeDevices.d.ts +2 -1
  27. package/dist/hooks/useScoutRealtimeDevices.js +3 -64
  28. package/dist/hooks/useScoutRealtimeDocuments.d.ts +3 -3
  29. package/dist/hooks/useScoutRealtimeDocuments.js +7 -5
  30. package/dist/hooks/useScoutRealtimeEvents.d.ts +2 -1
  31. package/dist/hooks/useScoutRealtimeEvents.js +3 -69
  32. package/dist/hooks/useScoutRealtimeMaintenanceRequests.d.ts +2 -1
  33. package/dist/hooks/useScoutRealtimeMaintenanceRequests.js +3 -3
  34. package/dist/hooks/useScoutRealtimeOperatingContexts.d.ts +12 -0
  35. package/dist/hooks/useScoutRealtimeOperatingContexts.js +26 -0
  36. package/dist/hooks/useScoutRealtimeParts.d.ts +2 -1
  37. package/dist/hooks/useScoutRealtimeParts.js +3 -3
  38. package/dist/hooks/useScoutRealtimePins.d.ts +2 -1
  39. package/dist/hooks/useScoutRealtimePins.js +3 -69
  40. package/dist/hooks/useScoutRealtimePlans.d.ts +2 -1
  41. package/dist/hooks/useScoutRealtimePlans.js +3 -3
  42. package/dist/hooks/useScoutRealtimeSessionIncidents.d.ts +2 -1
  43. package/dist/hooks/useScoutRealtimeSessionIncidents.js +3 -3
  44. package/dist/hooks/useScoutRealtimeSessions.d.ts +2 -1
  45. package/dist/hooks/useScoutRealtimeSessions.js +3 -69
  46. package/dist/hooks/useScoutRealtimeTags.d.ts +2 -1
  47. package/dist/hooks/useScoutRealtimeTags.js +3 -69
  48. package/dist/hooks/useScoutRealtimeVersionsSoftware.d.ts +2 -1
  49. package/dist/hooks/useScoutRealtimeVersionsSoftware.js +3 -5
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.js +2 -0
  52. package/dist/providers/ScoutRefreshProvider.d.ts +1 -0
  53. package/dist/providers/ScoutRefreshProvider.js +3 -0
  54. package/dist/store/api.js +14 -34
  55. package/dist/types/db.d.ts +85 -0
  56. package/dist/types/realtime.d.ts +1 -0
  57. package/dist/types/supabase.d.ts +967 -100
  58. package/dist/types/supabase.js +26 -1
  59. package/package.json +1 -1
@@ -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
  }
@@ -0,0 +1,12 @@
1
+ import { SupabaseClient } from "@supabase/supabase-js";
2
+ import { Database } from "../types/supabase";
3
+ import { IOperatingContext, IOperatingContextEquipmentRequirement, IOperatingContextPayload, IOperatingContextRegion, IOperatingContextRisk, IOperatingContextRoleAssignment } from "../types/db";
4
+ import { RealtimeData } from "../types/realtime";
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export type OperatingContextRealtimeRow = IOperatingContext | IOperatingContextPayload | IOperatingContextRoleAssignment | IOperatingContextEquipmentRequirement | IOperatingContextRegion | IOperatingContextRisk;
7
+ type OperatingContextRealtimeChange<T, Table extends string> = Omit<RealtimeData<T>, "table"> & {
8
+ table: Table;
9
+ };
10
+ export type OperatingContextRealtimeData = OperatingContextRealtimeChange<IOperatingContext, "operating_contexts"> | OperatingContextRealtimeChange<IOperatingContextPayload, "operating_context_payloads"> | OperatingContextRealtimeChange<IOperatingContextRoleAssignment, "operating_context_role_assignments"> | OperatingContextRealtimeChange<IOperatingContextEquipmentRequirement, "operating_context_equipment_requirements"> | OperatingContextRealtimeChange<IOperatingContextRegion, "operating_context_regions"> | OperatingContextRealtimeChange<IOperatingContextRisk, "operating_context_risks">;
11
+ export declare function useScoutRealtimeOperatingContexts(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [OperatingContextRealtimeData | null, () => void];
12
+ export {};
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ import { useSelector } from "react-redux";
4
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
5
+ const TABLE_MARKERS = [
6
+ ["operating_contexts", "operating_context_type_id"],
7
+ ["operating_context_payloads", "payload_type"],
8
+ ["operating_context_role_assignments", "membership_id"],
9
+ ["operating_context_equipment_requirements", "equipment_item_id"],
10
+ ["operating_context_regions", "is_exclusion"],
11
+ ["operating_context_risks", "probability_before"],
12
+ ];
13
+ function discriminate(update) {
14
+ if (!update)
15
+ return null;
16
+ const marker = TABLE_MARKERS.find(([table]) => table === update.table)?.[1];
17
+ if (!marker || !(marker in update.data))
18
+ return null;
19
+ return update;
20
+ }
21
+ export function useScoutRealtimeOperatingContexts(scoutSupabase, options) {
22
+ const activeHerdId = useSelector((state) => state.scout.active_herd_id);
23
+ const [update, clear] = useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-operating_contexts`] : [], options);
24
+ const discriminated = useMemo(() => discriminate(update), [update]);
25
+ return [discriminated, clear];
26
+ }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IPart } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimeParts(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IPart> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimeParts(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IPart> | 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 useScoutRealtimeParts(scoutSupabase) {
3
+ import { useScoutRealtimeBroadcast, } from "./useScoutRealtimeBroadcast";
4
+ export function useScoutRealtimeParts(scoutSupabase, options) {
5
5
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
6
- return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-parts`] : []);
6
+ return useScoutRealtimeBroadcast(scoutSupabase, activeHerdId ? [`${activeHerdId}-parts`] : [], options);
7
7
  }
@@ -2,4 +2,5 @@ import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
3
  import { IPin } from "../types/db";
4
4
  import { RealtimeData } from "../types/realtime";
5
- export declare function useScoutRealtimePins(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IPin> | null, () => void];
5
+ import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
6
+ export declare function useScoutRealtimePins(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IPin> | null, () => void];