@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.
- package/dist/helpers/artifactMedia.d.ts +5 -0
- package/dist/helpers/artifactMedia.js +31 -0
- package/dist/helpers/artifacts.js +13 -19
- package/dist/helpers/documents.d.ts +1 -0
- package/dist/helpers/documents.js +11 -2
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/operating_contexts.d.ts +43 -0
- package/dist/helpers/operating_contexts.js +222 -0
- package/dist/helpers/realtime_topic_access.d.ts +5 -0
- package/dist/helpers/realtime_topic_access.js +55 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useAuthorizedRealtimeTopics.d.ts +1 -0
- package/dist/hooks/useAuthorizedRealtimeTopics.js +14 -0
- package/dist/hooks/useScoutRealtimeAnalysisJobs.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeAnalysisJobs.js +3 -5
- package/dist/hooks/useScoutRealtimeAnalysisTasks.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeAnalysisTasks.js +3 -5
- package/dist/hooks/useScoutRealtimeBroadcast.d.ts +9 -5
- package/dist/hooks/useScoutRealtimeBroadcast.js +118 -25
- package/dist/hooks/useScoutRealtimeConnectivity.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeConnectivity.js +18 -76
- package/dist/hooks/useScoutRealtimeContacts.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeContacts.js +3 -3
- package/dist/hooks/useScoutRealtimeDevices.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeDevices.js +3 -64
- package/dist/hooks/useScoutRealtimeDocuments.d.ts +3 -3
- package/dist/hooks/useScoutRealtimeDocuments.js +7 -5
- package/dist/hooks/useScoutRealtimeEvents.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeEvents.js +3 -69
- package/dist/hooks/useScoutRealtimeMaintenanceRequests.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeMaintenanceRequests.js +3 -3
- package/dist/hooks/useScoutRealtimeOperatingContexts.d.ts +12 -0
- package/dist/hooks/useScoutRealtimeOperatingContexts.js +26 -0
- package/dist/hooks/useScoutRealtimeParts.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeParts.js +3 -3
- package/dist/hooks/useScoutRealtimePins.d.ts +2 -1
- package/dist/hooks/useScoutRealtimePins.js +3 -69
- package/dist/hooks/useScoutRealtimePlans.d.ts +2 -1
- package/dist/hooks/useScoutRealtimePlans.js +3 -3
- package/dist/hooks/useScoutRealtimeSessionIncidents.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeSessionIncidents.js +3 -3
- package/dist/hooks/useScoutRealtimeSessions.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeSessions.js +3 -69
- package/dist/hooks/useScoutRealtimeTags.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeTags.js +3 -69
- package/dist/hooks/useScoutRealtimeVersionsSoftware.d.ts +2 -1
- package/dist/hooks/useScoutRealtimeVersionsSoftware.js +3 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/ScoutRefreshProvider.d.ts +1 -0
- package/dist/providers/ScoutRefreshProvider.js +3 -0
- package/dist/store/api.js +14 -34
- package/dist/types/db.d.ts +85 -0
- package/dist/types/realtime.d.ts +1 -0
- package/dist/types/supabase.d.ts +967 -100
- package/dist/types/supabase.js +26 -1
- package/package.json +1 -1
|
@@ -1,44 +1,137 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useCallback, useEffect,
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
import { ScoutRealtimeBroadcastOptions } from "./useScoutRealtimeBroadcast";
|
|
6
|
+
export declare function useScoutRealtimePins(scoutSupabase: SupabaseClient<Database>, options?: ScoutRealtimeBroadcastOptions): [RealtimeData<IPin> | null, () => void];
|