@vira-ui/cli 0.3.3-alpha → 0.4.0-alpha
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/go/appYaml.js +34 -0
- package/dist/go/backendEnvExample.js +21 -0
- package/dist/go/backendReadme.js +18 -0
- package/dist/go/channelHelpers.js +29 -0
- package/dist/go/configGo.js +262 -0
- package/dist/go/dbGo.js +47 -0
- package/dist/go/dbYaml.js +11 -0
- package/dist/go/dockerCompose.js +38 -0
- package/dist/go/dockerComposeProd.js +54 -0
- package/dist/go/dockerfile.js +19 -0
- package/dist/go/eventHandlerTemplate.js +34 -0
- package/dist/go/eventsAPI.js +414 -0
- package/dist/go/goMod.js +20 -0
- package/dist/go/kafkaGo.js +71 -0
- package/dist/go/kafkaYaml.js +10 -0
- package/dist/go/kanbanHandlers.js +221 -0
- package/dist/go/mainGo.js +527 -0
- package/dist/go/readme.js +14 -0
- package/dist/go/redisGo.js +35 -0
- package/dist/go/redisYaml.js +8 -0
- package/dist/go/registryGo.js +47 -0
- package/dist/go/sqlcYaml.js +17 -0
- package/dist/go/stateStore.js +119 -0
- package/dist/go/typesGo.js +15 -0
- package/dist/go/useViraState.js +160 -0
- package/dist/go/useViraStream.js +167 -0
- package/dist/index.js +608 -190
- package/dist/react/appTsx.js +52 -0
- package/dist/react/envExample.js +7 -0
- package/dist/react/envLocal.js +5 -0
- package/dist/react/indexCss.js +22 -0
- package/dist/react/indexHtml.js +16 -0
- package/dist/react/kanbanAppTsx.js +34 -0
- package/dist/react/kanbanBoard.js +63 -0
- package/dist/react/kanbanCard.js +65 -0
- package/dist/react/kanbanColumn.js +67 -0
- package/dist/react/kanbanModels.js +37 -0
- package/dist/react/kanbanService.js +119 -0
- package/dist/react/mainTsx.js +16 -0
- package/dist/react/tsconfig.js +25 -0
- package/dist/react/viteConfig.js +31 -0
- package/package.json +3 -4
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stateStore = void 0;
|
|
4
|
+
exports.stateStore = `package events
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"context"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"fmt"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/redis/go-redis/v9"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// StateStore abstracts persist/replay of channel state.
|
|
16
|
+
type StateStore interface {
|
|
17
|
+
SaveSnapshot(ctx context.Context, channel string, snapshot StateSnapshot, ttlSec int) error
|
|
18
|
+
LoadSnapshot(ctx context.Context, channel string) (StateSnapshot, bool, error)
|
|
19
|
+
AppendHistory(ctx context.Context, channel string, snapshot StateSnapshot, maxLen int, ttlSec int) error
|
|
20
|
+
LoadHistory(ctx context.Context, channel string, fromVersion int64) ([]StateSnapshot, error)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// MemoryStore fallback (no-op persist)
|
|
24
|
+
type MemoryStore struct{}
|
|
25
|
+
|
|
26
|
+
func (MemoryStore) SaveSnapshot(ctx context.Context, channel string, snapshot StateSnapshot, ttlSec int) error { return nil }
|
|
27
|
+
func (MemoryStore) LoadSnapshot(ctx context.Context, channel string) (StateSnapshot, bool, error) {
|
|
28
|
+
return StateSnapshot{}, false, nil
|
|
29
|
+
}
|
|
30
|
+
func (MemoryStore) AppendHistory(ctx context.Context, channel string, snapshot StateSnapshot, maxLen int, ttlSec int) error {
|
|
31
|
+
return nil
|
|
32
|
+
}
|
|
33
|
+
func (MemoryStore) LoadHistory(ctx context.Context, channel string, fromVersion int64) ([]StateSnapshot, error) {
|
|
34
|
+
return nil, nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// RedisStore persists snapshots/history into Redis.
|
|
38
|
+
type RedisStore struct {
|
|
39
|
+
client *redis.Client
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func NewRedisStore(client *redis.Client) *RedisStore {
|
|
43
|
+
return &RedisStore{client: client}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Keys:
|
|
47
|
+
// snapshot:<channel> -> json StateSnapshot
|
|
48
|
+
// history:<channel> -> list of json StateSnapshot (trimmed)
|
|
49
|
+
|
|
50
|
+
func snapshotKey(ch string) string { return fmt.Sprintf("snapshot:%s", ch) }
|
|
51
|
+
func historyKey(ch string) string { return fmt.Sprintf("history:%s", ch) }
|
|
52
|
+
|
|
53
|
+
func (s *RedisStore) SaveSnapshot(ctx context.Context, channel string, snapshot StateSnapshot, ttlSec int) error {
|
|
54
|
+
raw, err := json.Marshal(snapshot)
|
|
55
|
+
if err != nil {
|
|
56
|
+
return err
|
|
57
|
+
}
|
|
58
|
+
key := snapshotKey(channel)
|
|
59
|
+
if err := s.client.Set(ctx, key, raw, time.Duration(ttlSec)*time.Second).Err(); err != nil {
|
|
60
|
+
return err
|
|
61
|
+
}
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (s *RedisStore) LoadSnapshot(ctx context.Context, channel string) (StateSnapshot, bool, error) {
|
|
66
|
+
key := snapshotKey(channel)
|
|
67
|
+
val, err := s.client.Get(ctx, key).Bytes()
|
|
68
|
+
if err == redis.Nil {
|
|
69
|
+
return StateSnapshot{}, false, nil
|
|
70
|
+
}
|
|
71
|
+
if err != nil {
|
|
72
|
+
return StateSnapshot{}, false, err
|
|
73
|
+
}
|
|
74
|
+
var snap StateSnapshot
|
|
75
|
+
if err := json.Unmarshal(val, &snap); err != nil {
|
|
76
|
+
return StateSnapshot{}, false, err
|
|
77
|
+
}
|
|
78
|
+
return snap, true, nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func (s *RedisStore) AppendHistory(ctx context.Context, channel string, snapshot StateSnapshot, maxLen int, ttlSec int) error {
|
|
82
|
+
raw, err := json.Marshal(snapshot)
|
|
83
|
+
if err != nil {
|
|
84
|
+
return err
|
|
85
|
+
}
|
|
86
|
+
key := historyKey(channel)
|
|
87
|
+
pipe := s.client.Pipeline()
|
|
88
|
+
pipe.RPush(ctx, key, raw)
|
|
89
|
+
if maxLen > 0 {
|
|
90
|
+
pipe.LTrim(ctx, key, int64(-maxLen), int64(-1))
|
|
91
|
+
}
|
|
92
|
+
if ttlSec > 0 {
|
|
93
|
+
pipe.Expire(ctx, key, time.Duration(ttlSec)*time.Second)
|
|
94
|
+
}
|
|
95
|
+
_, err = pipe.Exec(ctx)
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func (s *RedisStore) LoadHistory(ctx context.Context, channel string, fromVersion int64) ([]StateSnapshot, error) {
|
|
100
|
+
key := historyKey(channel)
|
|
101
|
+
items, err := s.client.LRange(ctx, key, 0, -1).Result()
|
|
102
|
+
if err == redis.Nil {
|
|
103
|
+
return nil, nil
|
|
104
|
+
}
|
|
105
|
+
if err != nil {
|
|
106
|
+
return nil, err
|
|
107
|
+
}
|
|
108
|
+
var out []StateSnapshot
|
|
109
|
+
for _, item := range items {
|
|
110
|
+
var snap StateSnapshot
|
|
111
|
+
if err := json.Unmarshal([]byte(item), &snap); err == nil {
|
|
112
|
+
if snap.VersionNo > fromVersion {
|
|
113
|
+
out = append(out, snap)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out, nil
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.typesGo = void 0;
|
|
4
|
+
exports.typesGo = `package types
|
|
5
|
+
|
|
6
|
+
import "time"
|
|
7
|
+
|
|
8
|
+
// Example shared model; extend with your domain structs.
|
|
9
|
+
type User struct {
|
|
10
|
+
ID int \`json:"id"\`
|
|
11
|
+
Username string \`json:"username"\`
|
|
12
|
+
Role string \`json:"role"\`
|
|
13
|
+
UpdatedAt time.Time \`json:"updated_at"\`
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useViraState = void 0;
|
|
4
|
+
exports.useViraState = `import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
type Message = { type: string; channel?: string; data?: any; patch?: any; ts?: number; session?: string; interval?: number; versionNo?: number };
|
|
7
|
+
const WS_PATH = '/ws';
|
|
8
|
+
|
|
9
|
+
export function useViraState<T = any, C extends string = string>(channel: C, initial: T | null = null) {
|
|
10
|
+
const [data, setData] = useState<T | null>(initial);
|
|
11
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
12
|
+
const sessionRef = useRef<string | null>(null);
|
|
13
|
+
const bufferRef = useRef<Array<{ send: () => void; ver: number }>>([]);
|
|
14
|
+
const versionRef = useRef<number>(0);
|
|
15
|
+
const lastSentVersionRef = useRef<number>(0);
|
|
16
|
+
const url = useMemo(() => {
|
|
17
|
+
const api = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
|
18
|
+
return api.replace(/^http/, 'ws') + WS_PATH;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const sendEvent = useCallback(
|
|
22
|
+
(name: string, payload: any) => {
|
|
23
|
+
const ver = versionRef.current;
|
|
24
|
+
const send = () =>
|
|
25
|
+
wsRef.current?.send(
|
|
26
|
+
JSON.stringify({ type: 'event', name, channel, data: payload, ts: Date.now(), versionNo: ver })
|
|
27
|
+
);
|
|
28
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
29
|
+
send();
|
|
30
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
31
|
+
} else {
|
|
32
|
+
bufferRef.current.push({ send, ver });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[channel]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const sendUpdate = useCallback(
|
|
39
|
+
(payload: any) => {
|
|
40
|
+
const ver = versionRef.current;
|
|
41
|
+
const send = () =>
|
|
42
|
+
wsRef.current?.send(
|
|
43
|
+
JSON.stringify({ type: 'update', channel, data: payload, ts: Date.now(), versionNo: ver })
|
|
44
|
+
);
|
|
45
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
46
|
+
send();
|
|
47
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
48
|
+
} else {
|
|
49
|
+
bufferRef.current.push({ send, ver });
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[channel]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const sendDiff = useCallback(
|
|
56
|
+
(patch: any) => {
|
|
57
|
+
const ver = versionRef.current;
|
|
58
|
+
const send = () =>
|
|
59
|
+
wsRef.current?.send(
|
|
60
|
+
JSON.stringify({ type: 'diff', channel, patch, ts: Date.now(), versionNo: ver })
|
|
61
|
+
);
|
|
62
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
63
|
+
send();
|
|
64
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
65
|
+
} else {
|
|
66
|
+
bufferRef.current.push({ send, ver });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[channel]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!channel) return;
|
|
74
|
+
let closed = false;
|
|
75
|
+
let timeout = 500;
|
|
76
|
+
|
|
77
|
+
const connect = () => {
|
|
78
|
+
if (closed) return;
|
|
79
|
+
const ws = new WebSocket(url);
|
|
80
|
+
wsRef.current = ws;
|
|
81
|
+
|
|
82
|
+
ws.onopen = () => {
|
|
83
|
+
ws.send(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
type: 'handshake',
|
|
86
|
+
client: 'vira-react',
|
|
87
|
+
version: '0.1',
|
|
88
|
+
authToken: import.meta.env.VITE_AUTH_TOKEN || '',
|
|
89
|
+
ts: Date.now(),
|
|
90
|
+
session: sessionRef.current || undefined,
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
ws.send(JSON.stringify({ type: 'sub', channels: [channel] }));
|
|
94
|
+
// flush buffered messages
|
|
95
|
+
bufferRef.current.splice(0).forEach(({ send, ver }) => {
|
|
96
|
+
if (ver <= lastSentVersionRef.current) return;
|
|
97
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
98
|
+
send();
|
|
99
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
ws.onmessage = (evt) => {
|
|
105
|
+
try {
|
|
106
|
+
const msg = JSON.parse(evt.data as string) as Message;
|
|
107
|
+
if (msg.type === 'ack' && msg.session) {
|
|
108
|
+
sessionRef.current = msg.session;
|
|
109
|
+
}
|
|
110
|
+
if ((msg.type === 'update' || msg.type === 'event') && msg.channel === channel) {
|
|
111
|
+
if (msg.versionNo && msg.versionNo < versionRef.current) return;
|
|
112
|
+
if (msg.versionNo) versionRef.current = msg.versionNo;
|
|
113
|
+
setData(msg.data as T);
|
|
114
|
+
}
|
|
115
|
+
if (msg.type === 'diff' && msg.channel === channel && msg.patch && typeof msg.patch === 'object') {
|
|
116
|
+
if (msg.versionNo && msg.versionNo < versionRef.current) return;
|
|
117
|
+
if (msg.versionNo) versionRef.current = msg.versionNo;
|
|
118
|
+
setData((prev) => {
|
|
119
|
+
if (prev && typeof prev === 'object') {
|
|
120
|
+
return { ...(prev as any), ...(msg.patch as any) };
|
|
121
|
+
}
|
|
122
|
+
return (msg.patch as T) || prev;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (msg.type === 'ping') {
|
|
126
|
+
ws.send(JSON.stringify({ type: 'pong', ts: Date.now() }));
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore malformed
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
ws.onclose = () => {
|
|
134
|
+
wsRef.current = null;
|
|
135
|
+
if (closed) return;
|
|
136
|
+
setTimeout(connect, timeout);
|
|
137
|
+
timeout = Math.min(timeout * 2, 5000);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
ws.onerror = () => {
|
|
141
|
+
ws.close();
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
connect();
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
closed = true;
|
|
149
|
+
wsRef.current?.close();
|
|
150
|
+
};
|
|
151
|
+
}, [channel, url]);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
data,
|
|
155
|
+
sendEvent,
|
|
156
|
+
sendUpdate,
|
|
157
|
+
sendDiff,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useViraStream = void 0;
|
|
4
|
+
exports.useViraStream = `import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
type Message = { type: string; channel?: string; data?: any; session?: string; interval?: number; versionNo?: number };
|
|
7
|
+
const WS_PATH = '/ws';
|
|
8
|
+
|
|
9
|
+
export function useViraStream<T = any, C extends string = string>(channel: C) {
|
|
10
|
+
const [data, setData] = useState<T | null>(null);
|
|
11
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
12
|
+
const sessionRef = useRef<string | null>(null);
|
|
13
|
+
const bufferRef = useRef<Array<{ msg: any; ver: number }>>([]);
|
|
14
|
+
const versionRef = useRef<number>(0);
|
|
15
|
+
const lastSentVersionRef = useRef<number>(0);
|
|
16
|
+
const url = useMemo(() => {
|
|
17
|
+
const api = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
|
18
|
+
return api.replace(/^http/, 'ws') + WS_PATH;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const sendEvent = useCallback(
|
|
22
|
+
(name: string, payload: any, msgId?: string) => {
|
|
23
|
+
const ver = versionRef.current;
|
|
24
|
+
const msg: any = { type: 'event', name, channel, data: payload, ts: Date.now(), versionNo: ver };
|
|
25
|
+
if (msgId) msg.msgId = msgId;
|
|
26
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
27
|
+
wsRef.current.send(JSON.stringify(msg));
|
|
28
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
29
|
+
} else {
|
|
30
|
+
bufferRef.current.push({ msg, ver });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
[channel]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const sendUpdate = useCallback(
|
|
37
|
+
(payload: any, msgId?: string) => {
|
|
38
|
+
const ver = versionRef.current;
|
|
39
|
+
const msg: any = { type: 'update', channel, data: payload, ts: Date.now(), versionNo: ver };
|
|
40
|
+
if (msgId) msg.msgId = msgId;
|
|
41
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
42
|
+
wsRef.current.send(JSON.stringify(msg));
|
|
43
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
44
|
+
} else {
|
|
45
|
+
bufferRef.current.push({ msg, ver });
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[channel]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const sendDiff = useCallback(
|
|
52
|
+
(patch: any, msgId?: string) => {
|
|
53
|
+
const ver = versionRef.current;
|
|
54
|
+
const msg: any = { type: 'diff', channel, patch, ts: Date.now(), versionNo: ver };
|
|
55
|
+
if (msgId) msg.msgId = msgId;
|
|
56
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
57
|
+
wsRef.current.send(JSON.stringify(msg));
|
|
58
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
59
|
+
} else {
|
|
60
|
+
bufferRef.current.push({ msg, ver });
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[channel]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!channel) return;
|
|
68
|
+
let closed = false;
|
|
69
|
+
let timeout = 500;
|
|
70
|
+
let pingIntervalId: any;
|
|
71
|
+
|
|
72
|
+
const connect = () => {
|
|
73
|
+
if (closed) return;
|
|
74
|
+
const ws = new WebSocket(url);
|
|
75
|
+
wsRef.current = ws;
|
|
76
|
+
|
|
77
|
+
ws.onopen = () => {
|
|
78
|
+
console.log(\`WS connected to \${url}\`);
|
|
79
|
+
ws.send(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
type: 'handshake',
|
|
82
|
+
client: 'vira-react',
|
|
83
|
+
version: '0.1',
|
|
84
|
+
authToken: import.meta.env.VITE_AUTH_TOKEN || '',
|
|
85
|
+
session: sessionRef.current || undefined,
|
|
86
|
+
ts: Date.now(),
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
ws.send(JSON.stringify({ type: 'sub', channels: [channel] }));
|
|
90
|
+
// Resend buffered messages
|
|
91
|
+
const buffered = bufferRef.current.splice(0);
|
|
92
|
+
buffered.forEach(({ msg, ver }) => {
|
|
93
|
+
if (ver <= lastSentVersionRef.current) return;
|
|
94
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
95
|
+
ws.send(JSON.stringify(msg));
|
|
96
|
+
lastSentVersionRef.current = Math.max(lastSentVersionRef.current, ver);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
timeout = 500; // Reset timeout on successful connection
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
ws.onmessage = (evt) => {
|
|
103
|
+
try {
|
|
104
|
+
const msg = JSON.parse(evt.data as string) as Message;
|
|
105
|
+
if (msg.type === 'ack' && msg.session) {
|
|
106
|
+
sessionRef.current = msg.session;
|
|
107
|
+
if (msg.interval) {
|
|
108
|
+
clearInterval(pingIntervalId);
|
|
109
|
+
pingIntervalId = setInterval(() => {
|
|
110
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
111
|
+
ws.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
|
|
112
|
+
}
|
|
113
|
+
}, msg.interval);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if ((msg.type === 'update' || msg.type === 'event') && msg.channel === channel) {
|
|
117
|
+
if (msg.versionNo && msg.versionNo < versionRef.current) return;
|
|
118
|
+
if (msg.versionNo) versionRef.current = msg.versionNo;
|
|
119
|
+
setData(msg.data as T);
|
|
120
|
+
}
|
|
121
|
+
if (msg.type === 'diff' && msg.channel === channel && msg.patch && typeof msg.patch === 'object') {
|
|
122
|
+
setData((prev) => {
|
|
123
|
+
if (prev && typeof prev === 'object') {
|
|
124
|
+
return { ...(prev as any), ...(msg.patch as any) };
|
|
125
|
+
}
|
|
126
|
+
return (msg.patch as T) || prev;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (msg.type === 'ping') {
|
|
130
|
+
ws.send(JSON.stringify({ type: 'pong', ts: Date.now() }));
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore malformed
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
ws.onclose = () => {
|
|
138
|
+
console.log('WS disconnected. Reconnecting...');
|
|
139
|
+
wsRef.current = null;
|
|
140
|
+
clearInterval(pingIntervalId);
|
|
141
|
+
if (closed) return;
|
|
142
|
+
setTimeout(connect, timeout);
|
|
143
|
+
timeout = Math.min(timeout * 2, 30000); // Max 30 seconds
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
ws.onerror = () => {
|
|
147
|
+
ws.close();
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
connect();
|
|
152
|
+
|
|
153
|
+
return () => {
|
|
154
|
+
closed = true;
|
|
155
|
+
clearInterval(pingIntervalId);
|
|
156
|
+
wsRef.current?.close();
|
|
157
|
+
};
|
|
158
|
+
}, [channel, url]);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
data,
|
|
162
|
+
sendEvent,
|
|
163
|
+
sendUpdate,
|
|
164
|
+
sendDiff,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
`;
|