11import type { SubscriptionMessage , SubscriptionUpdateMessage } from './utils/realtime'
22// @ts -expect-error virtual module
3- import { wsApiPath } from '#build/$rstore-drizzle-config.js'
3+ import { wsAutoReconnect , wsClientEndpoint , wsHeartbeatInterval } from '#build/$rstore-drizzle-config.js'
44import { definePlugin , realtimeReconnectEventHook } from '@rstore/vue'
55import { useWebSocket } from '@vueuse/core'
66import { watch } from 'vue'
7+ import { getRstoreDrizzleClientId } from './utils/client-id'
78import { getSubscriptionId } from './utils/realtime'
89
910export default definePlugin ( {
@@ -16,14 +17,20 @@ export default definePlugin({
1617
1718 setup ( { hook } ) {
1819 if ( import . meta. client ) {
20+ const clientId = getRstoreDrizzleClientId ( )
21+
22+ // Local ref-count per subscription id — a given (collection, key, where)
23+ // is only sent to the server once, regardless of how many components
24+ // subscribe to it locally. The matching `unsubscribe` is only emitted
25+ // when the last subscriber is torn down.
1926 const countPerTopic : Record < string , number > = { }
2027 const messages = new Map < string , SubscriptionMessage > ( )
2128
22- const ws = useWebSocket ( wsApiPath , {
29+ const ws = useWebSocket ( wsClientEndpoint , {
2330 heartbeat : {
24- interval : 10000 ,
31+ interval : wsHeartbeatInterval ,
2532 } ,
26- autoReconnect : true ,
33+ autoReconnect : wsAutoReconnect ,
2734 } )
2835
2936 let connectCount = 0
@@ -56,8 +63,14 @@ export default definePlugin({
5663 where,
5764 }
5865 const subscriptionId = getSubscriptionId ( message )
59- countPerTopic [ subscriptionId ] ??= 1
60- countPerTopic [ subscriptionId ] --
66+ const current = countPerTopic [ subscriptionId ] ?? 0
67+ // Guard against decrementing below zero — a stray unsubscribe without
68+ // a matching subscribe would otherwise send a spurious unsubscribe
69+ // frame and corrupt the counter.
70+ if ( current <= 0 ) {
71+ return
72+ }
73+ countPerTopic [ subscriptionId ] = current - 1
6174 if ( countPerTopic [ subscriptionId ] === 0 ) {
6275 ws . send ( JSON . stringify ( {
6376 subscription : message ,
@@ -67,8 +80,8 @@ export default definePlugin({
6780 } )
6881
6982 hook ( 'init' , ( { store } ) => {
70- watch ( ws . data , async ( data : string ) => {
71- if ( data === 'pong' ) {
83+ watch ( ws . data , async ( data ) => {
84+ if ( typeof data !== 'string' || data === 'pong' ) {
7285 return
7386 }
7487 try {
@@ -112,23 +125,26 @@ export default definePlugin({
112125 } )
113126
114127 watch ( ws . status , ( status ) => {
115- if ( status === 'CLOSED' ) {
116- // Reset counts on reconnect
117- for ( const key in countPerTopic ) {
118- countPerTopic [ key ] = 0
119- }
120- }
121- else if ( status === 'OPEN' ) {
128+ if ( status === 'OPEN' ) {
122129 connectCount ++
123130
124- // Resubscribe to all topics
131+ // Announce our clientId first so the server can tag us for
132+ // skip-self echo suppression before any mutation echoes arrive.
133+ if ( clientId ) {
134+ ws . send ( JSON . stringify ( { init : { clientId } } ) , false )
135+ }
136+
137+ // Resubscribe to all active topics. On first open this replays
138+ // subscriptions issued while CONNECTING; on reconnect it restores
139+ // subscriptions across a transient disconnect.
125140 for ( const [ , message ] of messages ) {
126141 ws . send ( JSON . stringify ( {
127142 subscription : message ,
128143 } ) , false )
129144 }
130145
131- // Call reconnect hook
146+ // Notify live queries to refresh so updates missed while offline
147+ // are recovered. Only fire on true reconnects (skip first open).
132148 if ( connectCount > 1 ) {
133149 realtimeReconnectEventHook . trigger ( )
134150 }
0 commit comments