Refactor WebSocket handling and remove nuxt-visitors module

Replaced nuxt-visitors module with a custom WebSocket implementation for location tracking. Removed redundant code and comments in `useVisitors` composable and introduced the `location.server` plugin to manage user location. Updated dependencies and configurations to reflect these changes, streamlining the approach.
This commit is contained in:
2025-02-02 19:03:49 +01:00
parent 523a4a0582
commit 6ab4ffc092
10 changed files with 53 additions and 101 deletions

View File

@@ -57,7 +57,7 @@ async function toggleTheme() {
}
const { locale, setLocale, locales, t } = useI18n()
const currentLocale = computed(() => locales.filter(l => l.code === locale.value)[0])
const currentLocale = computed(() => locales.value.filter(l => l.code === locale.value)[0])
const lang = ref(locale.value)
watch(lang, () => changeLocale(lang.value))

View File

@@ -34,7 +34,7 @@ const DEFAULT_CONFIG: COBEOptions = {
mapBrightness: 1,
baseColor: [0.8, 0.8, 0.8],
opacity: 0.7,
markerColor: [251 / 255, 100 / 255, 21 / 255],
markerColor: [160 / 255, 160 / 255, 160 / 255],
glowColor: [1, 1, 1],
markers: [],
}
@@ -90,7 +90,6 @@ function onRender(state: Record<string, unknown>) {
state.height = width.value * 2
state.markers = props.locations?.map(location => ({
location: [location.latitude, location.longitude],
// Set the size of the marker to 0.1 if it's the user's location, otherwise 0.05
size: props.myLocation?.latitude === location.latitude && props.myLocation?.longitude === location.longitude ? 0.1 : 0.05,
}))
}

View File

@@ -2,7 +2,7 @@
import type { Stats } from '~~/types'
const { locale, locales } = useI18n()
const currentLocale = computed(() => locales.find(l => l.code === locale.value))
const currentLocale = computed(() => locales.value.find(l => l.code === locale.value))
const { data: stats } = await useFetch<Stats>('/api/stats')
const { t } = useI18n({

View File

@@ -1,25 +1,7 @@
import { useState } from '#imports'
import { onBeforeUnmount, onMounted, ref } from 'vue'
/**
* Composable for tracking real-time website visitors count via WebSocket
*
* Features:
* - Real-time visitors count updates
* - Automatic WebSocket connection management
* - Connection status tracking
* - Error handling
* - Automatic cleanup on component unmount
*
* @returns {object} An object containing:
* - visitors: Ref<number> - Current number of visitors
* - isLoading: Ref<boolean> - Loading state indicator
* - error: Ref<string | null> - Error message if any
* - isConnected: Ref<boolean> - WebSocket connection status
* - reconnect: () => void - Function to manually reconnect
*/
export function useVisitors() {
// State management
const visitors = useState<number>('visitors', () => 0) // Added default value
const locations = ref<Array<{ latitude: number, longitude: number }>>([])
const myLocation = useState('location', () => ({
@@ -32,23 +14,15 @@ export function useVisitors() {
const isConnected = ref(false)
const isMounted = ref(true)
// Constants
const RECONNECTION_DELAY = 5000 // 5 seconds delay for reconnection
const WS_NORMAL_CLOSURE = 1000
/**
* Constructs the WebSocket URL based on the current protocol and host
* @returns {string} WebSocket URL
*/
const getWebSocketUrl = (): string => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const baseUrl = window.location.host.replace(/^(http|https):\/\//, '')
return `${protocol}//${baseUrl}/.nuxt-visitors/ws?latitude=${myLocation.value.latitude}&longitude=${myLocation.value.longitude}`
return `${protocol}//${baseUrl}/ws?latitude=${myLocation.value.latitude}&longitude=${myLocation.value.longitude}`
}
/**
* Cleans up WebSocket connection and resets state
*/
const cleanup = () => {
if (wsRef.value) {
wsRef.value.close()
@@ -58,10 +32,6 @@ export function useVisitors() {
isLoading.value = false
}
/**
* Handles WebSocket messages
* @param {MessageEvent} event - WebSocket message event
*/
const handleMessage = async (event: MessageEvent) => {
if (!isMounted.value)
return
@@ -83,10 +53,6 @@ export function useVisitors() {
}
}
/**
* Handles WebSocket connection closure
* @param {CloseEvent} event - WebSocket close event
*/
const handleClose = (event: CloseEvent) => {
console.log('Visitors WebSocket closed:', event.code, event.reason)
isConnected.value = false
@@ -94,14 +60,11 @@ export function useVisitors() {
if (isMounted.value && event.code !== WS_NORMAL_CLOSURE) {
error.value = 'Connection lost'
// Attempt to reconnect after delay
setTimeout(() => reconnect(), RECONNECTION_DELAY)
// eslint-disable-next-line ts/no-use-before-define
setTimeout(reconnect, RECONNECTION_DELAY)
}
}
/**
* Initializes WebSocket connection
*/
const initWebSocket = () => {
if (!isMounted.value)
return
@@ -141,9 +104,6 @@ export function useVisitors() {
}
}
/**
* Manually triggers WebSocket reconnection
*/
const reconnect = () => {
if (!isMounted.value)
return
@@ -152,7 +112,6 @@ export function useVisitors() {
initWebSocket()
}
// Lifecycle hooks
onMounted(() => {
if (import.meta.client) {
isMounted.value = true

View File

@@ -17,8 +17,6 @@ const { myLocation, locations } = useVisitors()
<HomeActivity />
<HomeQuote />
<HomeCatchPhrase />
{{ locations }}
{{ myLocation }}
<HomeGlobe
:my-location
:locations

View File

@@ -0,0 +1,11 @@
export default defineNuxtPlugin(() => {
const event = useRequestEvent()
console.log('event', event)
useState('location', () => ({
latitude: event?.context.cf?.latitude || Math.random() * 180 - 90, // default to random latitude (only in dev)
longitude: event?.context.cf?.longitude || Math.random() * 360 - 180, // default to random longitude (only in dev)
}))
console.log(useState('location').value)
})

View File

@@ -74,10 +74,7 @@ export default defineNuxtConfig({
},
},
// Nuxt Visitors
visitors: {
locations: true,
},
plugins: ['~/plugins/location.server'],
// Nuxt Color Mode
colorMode: {

View File

@@ -24,7 +24,6 @@
"drizzle-orm": "^0.33.0",
"h3-zod": "^0.5.3",
"nuxt": "^3.15.4",
"nuxt-visitors": "^1.1.2",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",

66
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^3.2.0
version: 3.2.0(magicast@0.3.5)(rollup@4.32.1)
'@nuxtjs/i18n':
specifier: 9.0.0-rc.2
version: 9.0.0-rc.2(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(magicast@0.3.5)(rollup@4.32.1)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
specifier: 9.1.5
version: 9.1.5(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(magicast@0.3.5)(rollup@4.32.1)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
cobe:
specifier: ^0.6.3
version: 0.6.3
@@ -38,9 +38,6 @@ importers:
nuxt:
specifier: ^3.15.4
version: 3.15.4(@libsql/client@0.5.6(encoding@0.1.13))(@parcel/watcher@2.4.1)(@types/node@22.12.0)(better-sqlite3@11.8.1)(db0@0.2.3(@libsql/client@0.5.6(encoding@0.1.13))(better-sqlite3@11.8.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20250124.3)(@libsql/client@0.5.6(encoding@0.1.13))(@opentelemetry/api@1.9.0)(better-sqlite3@11.8.1)(pg@8.13.1)))(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20250124.3)(@libsql/client@0.5.6(encoding@0.1.13))(@opentelemetry/api@1.9.0)(better-sqlite3@11.8.1)(pg@8.13.1))(encoding@0.1.13)(eslint@9.19.0(jiti@2.4.2))(ioredis@5.4.1)(lightningcss@1.29.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.32.1)(sass@1.77.6)(terser@5.31.6)(typescript@5.7.3)(vite@6.0.11(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(sass@1.77.6)(terser@5.31.6)(yaml@2.7.0))(vue-tsc@2.2.0(typescript@5.7.3))(yaml@2.7.0)
nuxt-visitors:
specifier: ^1.1.2
version: 1.1.2(magicast@0.3.5)(rollup@4.32.1)
rehype-katex:
specifier: ^7.0.1
version: 7.0.1
@@ -1123,8 +1120,8 @@ packages:
'@internationalized/number@3.6.0':
resolution: {integrity: sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==}
'@intlify/bundle-utils@9.0.0':
resolution: {integrity: sha512-19dunbgM4wuCvi2xSai2PKhXkcKGjlbJhNWm9BCQWkUYcPmXwzptNWOE0O7OSrhNlEDxwpkHsJzZ/vLbCkpElw==}
'@intlify/bundle-utils@10.0.0':
resolution: {integrity: sha512-BR5yLOkF2dzrARTbAg7RGAIPcx9Aark7p1K/0O285F7rfzso9j2dsa+S4dA67clZ0rToZ10NSSTfbyUptVu7Bg==}
engines: {node: '>= 18'}
peerDependencies:
petite-vue-i18n: '*'
@@ -1167,8 +1164,8 @@ packages:
resolution: {integrity: sha512-DvpNSxiMrFqYMaGSRDDnQgO/L0MqNH4KWw9CUx8LRHHIdWp08En9DpmSRNpauUOxKpHAhyJJxx92BHZk9J84EQ==}
engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@5.3.1':
resolution: {integrity: sha512-76huP8TpMOtBMLsYYIMLNbqMPXJ7+Q6xcjP6495h/pmbOQ7sw/DB8E0OFvDFeIZ2571a4ylzJnz+KMuYbAs1xA==}
'@intlify/unplugin-vue-i18n@6.0.3':
resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==}
engines: {node: '>= 18'}
peerDependencies:
petite-vue-i18n: '*'
@@ -1184,14 +1181,14 @@ packages:
resolution: {integrity: sha512-8i3uRdAxCGzuHwfmHcVjeLQBtysQB2aXl/ojoagDut5/gY5lvWCQ2+cnl2TiqE/fXj/D8EhWG/SLKA7qz4a3QA==}
engines: {node: '>= 18'}
'@intlify/vue-i18n-extensions@7.0.0':
resolution: {integrity: sha512-MtvfJnb4aklpCU5Q/dkWkBT/vGsp3qERiPIwtTq5lX4PCLHtUprAJZp8wQj5ZcwDaFCU7+yVMjYbeXpIf927cA==}
'@intlify/vue-i18n-extensions@8.0.0':
resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==}
engines: {node: '>= 18'}
peerDependencies:
'@intlify/shared': ^9.0.0 || ^10.0.0
'@intlify/shared': ^9.0.0 || ^10.0.0 || ^11.0.0
'@vue/compiler-dom': ^3.0.0
vue: ^3.0.0
vue-i18n: ^9.0.0 || ^10.0.0
vue-i18n: ^9.0.0 || ^10.0.0 || ^11.0.0
peerDependenciesMeta:
'@intlify/shared':
optional: true
@@ -1427,8 +1424,8 @@ packages:
'@nuxtjs/google-fonts@3.2.0':
resolution: {integrity: sha512-cGAjDJoeQ2jm6VJCo4AtSmKO6KjsbO9RSLj8q261fD0lMVNMZCxkCxBkg8L0/2Vfgp+5QBHWVXL71p1tiybJFw==}
'@nuxtjs/i18n@9.0.0-rc.2':
resolution: {integrity: sha512-OIxPyci9Wvi/Ewhtc/xZ0XTsqqlm0KRQoY0JrwthdaSVKgyFcKRkP70YLpm7JmgcRDkQk/VkcCUFLDJoo3MiZw==}
'@nuxtjs/i18n@9.1.5':
resolution: {integrity: sha512-dFbo3etm5xqG3vF4sLeVrR+wXVcxBszDCds5xtJBSESS7riJBtW83BujSMUnalbRxvGOLhJj+b6Qb8vj7Im/9Q==}
engines: {node: ^14.16.0 || >=16.11.0}
'@nuxtjs/mdc@0.13.2':
@@ -4774,9 +4771,6 @@ packages:
resolution: {integrity: sha512-iq7hbSnfp4Ff/PTMYBF8pYabTQuF3u7HVN66Kb3hOnrnaPEdXEn/q6HkAn5V8UjOVSgXYpvycM0wSnwyADYNVA==}
hasBin: true
nuxt-visitors@1.1.2:
resolution: {integrity: sha512-gUSHTNhH0XDwo5/Op/+MrZe/inrLN++nuwLlMgwgciZmCQVrIbj8RXmtGAncVmeLKQim/n7gZlKWJbFASCq/rA==}
nuxt@3.15.4:
resolution: {integrity: sha512-hSbZO4mR0uAMJtZPNTnCfiAtgleoOu28gvJcBNU7KQHgWnNXPjlWgwMczko2O4Tmnv9zIe/CQged+2HsPwl2ZA==}
engines: {node: ^18.20.5 || ^20.9.0 || >=22.0.0}
@@ -6976,7 +6970,7 @@ snapshots:
'@babel/parser': 7.26.2
'@babel/template': 7.25.9
'@babel/types': 7.26.0
debug: 4.3.7
debug: 4.4.0(supports-color@9.4.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -7512,7 +7506,7 @@ snapshots:
dependencies:
'@swc/helpers': 0.5.15
'@intlify/bundle-utils@9.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))':
'@intlify/bundle-utils@10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))':
dependencies:
'@intlify/message-compiler': 11.0.0-rc.1
'@intlify/shared': 11.0.0-rc.1
@@ -7557,12 +7551,12 @@ snapshots:
'@intlify/shared@11.1.0': {}
'@intlify/unplugin-vue-i18n@5.3.1(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(rollup@4.32.1)(typescript@5.7.3)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
'@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(rollup@4.32.1)(typescript@5.7.3)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2))
'@intlify/bundle-utils': 9.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))
'@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))
'@intlify/shared': 11.1.0
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.1.0)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.0)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@rollup/pluginutils': 5.1.4(rollup@4.32.1)
'@typescript-eslint/scope-manager': 8.22.0
'@typescript-eslint/typescript-estree': 8.22.0(typescript@5.7.3)
@@ -7586,7 +7580,7 @@ snapshots:
'@intlify/utils@0.13.0': {}
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.1.0)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.0)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@babel/parser': 7.26.7
optionalDependencies:
@@ -8390,11 +8384,11 @@ snapshots:
- rollup
- supports-color
'@nuxtjs/i18n@9.0.0-rc.2(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(magicast@0.3.5)(rollup@4.32.1)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))':
'@nuxtjs/i18n@9.1.5(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(magicast@0.3.5)(rollup@4.32.1)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@intlify/h3': 0.6.1
'@intlify/shared': 10.0.5
'@intlify/unplugin-vue-i18n': 5.3.1(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(rollup@4.32.1)(typescript@5.7.3)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@intlify/unplugin-vue-i18n': 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(rollup@4.32.1)(typescript@5.7.3)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@intlify/utils': 0.13.0
'@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.32.1)
'@nuxt/kit': 3.15.4(magicast@0.3.5)(rollup@4.32.1)
@@ -11942,14 +11936,14 @@ snapshots:
dependencies:
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.0
micromark-util-types: 2.0.0
micromark-util-types: 2.0.1
micromark-factory-label@2.0.0:
dependencies:
devlop: 1.1.0
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.0
micromark-util-types: 2.0.0
micromark-util-types: 2.0.1
micromark-factory-space@2.0.0:
dependencies:
@@ -11963,17 +11957,17 @@ snapshots:
micromark-factory-title@2.0.0:
dependencies:
micromark-factory-space: 2.0.0
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.0
micromark-util-types: 2.0.0
micromark-util-types: 2.0.1
micromark-factory-whitespace@2.0.0:
dependencies:
micromark-factory-space: 2.0.0
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.0
micromark-util-types: 2.0.0
micromark-util-types: 2.0.1
micromark-factory-whitespace@2.0.1:
dependencies:
@@ -12445,14 +12439,6 @@ snapshots:
- rollup
- supports-color
nuxt-visitors@1.1.2(magicast@0.3.5)(rollup@4.32.1):
dependencies:
'@nuxt/kit': 3.15.2(magicast@0.3.5)(rollup@4.32.1)
transitivePeerDependencies:
- magicast
- rollup
- supports-color
nuxt@3.15.4(@libsql/client@0.5.6(encoding@0.1.13))(@parcel/watcher@2.4.1)(@types/node@22.12.0)(better-sqlite3@11.8.1)(db0@0.2.3(@libsql/client@0.5.6(encoding@0.1.13))(better-sqlite3@11.8.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20250124.3)(@libsql/client@0.5.6(encoding@0.1.13))(@opentelemetry/api@1.9.0)(better-sqlite3@11.8.1)(pg@8.13.1)))(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20250124.3)(@libsql/client@0.5.6(encoding@0.1.13))(@opentelemetry/api@1.9.0)(better-sqlite3@11.8.1)(pg@8.13.1))(encoding@0.1.13)(eslint@9.19.0(jiti@2.4.2))(ioredis@5.4.1)(lightningcss@1.29.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.32.1)(sass@1.77.6)(terser@5.31.6)(typescript@5.7.3)(vite@6.0.11(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(sass@1.77.6)(terser@5.31.6)(yaml@2.7.0))(vue-tsc@2.2.0(typescript@5.7.3))(yaml@2.7.0):
dependencies:
'@nuxt/cli': 3.21.1(magicast@0.3.5)

View File

@@ -1,17 +1,20 @@
import type { Peer } from 'crossws'
import { defineWebSocketHandler } from 'h3'
import { getQuery } from 'ufo'
export default defineWebSocketHandler({
open(peer) {
open(peer: Peer) {
const locations = Array.from(peer.peers.values()).map(peer => getQuery(peer.websocket.url!))
peer.subscribe('visitors')
peer.publish('visitors', JSON.stringify(locations))
peer.subscribe('nuxt-visitors')
peer.publish('nuxt-visitors', JSON.stringify(locations))
peer.send(JSON.stringify(locations))
},
close(peer) {
peer.unsubscribe('visitors')
close(peer: Peer) {
peer.unsubscribe('nuxt-visitors')
setTimeout(() => {
const locations = Array.from(peer.peers.values()).map(peer => getQuery(peer.websocket.url!))
peer.publish('visitors', JSON.stringify(locations))
peer.publish('nuxt-visitors', JSON.stringify(locations))
}, 500)
},
})