@omni-vue/stomp-websocket 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ export { useStomp } from "./useStomp";
2
+ export { useStompChannel } from "./useStompChannel";
3
+ export type { PayloadType, ConnectionStatus } from "./useStomp";
4
+ export type { SendConfig, StompChannelOptions } from "./useStompChannel";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { useStomp } from "./useStomp";
2
+ export { useStompChannel } from "./useStompChannel";
@@ -0,0 +1,23 @@
1
+ import { type Ref } from "vue";
2
+ export type PayloadType = {
3
+ from: string;
4
+ content: {
5
+ currentPage: number;
6
+ size: number;
7
+ total: number;
8
+ rows: Array<Record<string, any>>;
9
+ [key: string]: any;
10
+ };
11
+ };
12
+ export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
13
+ export declare function useStomp(url: string | Ref<string>): {
14
+ connectionStatus: Ref<ConnectionStatus, ConnectionStatus>;
15
+ messages: Ref<any[], any[]>;
16
+ connect: () => void;
17
+ disconnect: () => void;
18
+ subscribe: (topic: string, callback: (payload: any) => void) => string;
19
+ unsubscribe: (topic: string) => void;
20
+ send: (destination: string, body: object) => void;
21
+ isConnected: () => boolean;
22
+ };
23
+ //# sourceMappingURL=useStomp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStomp.d.ts","sourceRoot":"","sources":["../useStomp.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AACvD,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE;QACP,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;QACjC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,YAAY,GACZ,WAAW,GACX,OAAO,CAAC;AASZ,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;;;;;uBA4EtB,MAAM,YAAY,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI;yBAgBtC,MAAM;wBASP,MAAM,QAAQ,MAAM;;EAsBhD"}
@@ -0,0 +1,110 @@
1
+ import { Client } from "@stomp/stompjs";
2
+ import { ref, shallowRef, unref } from "vue";
3
+ export function useStomp(url) {
4
+ const connectionStatus = ref("disconnected");
5
+ const messages = ref([]);
6
+ // 使用 shallowRef 避免 Vue 深度代理 Client 对象带来的性能损耗
7
+ const stompClient = shallowRef(null);
8
+ // 维护订阅关系(断线重连时需要依赖这个进行自动重连)
9
+ const activeSubscriptions = new Map();
10
+ // 连接
11
+ const connect = () => {
12
+ if (stompClient.value?.active)
13
+ return; // 使用 active 判断是否已经在运行
14
+ connectionStatus.value = "connecting";
15
+ const client = new Client({
16
+ brokerURL: unref(url),
17
+ reconnectDelay: 5000,
18
+ heartbeatIncoming: 4000,
19
+ heartbeatOutgoing: 4000,
20
+ });
21
+ client.onConnect = (frame) => {
22
+ connectionStatus.value = "connected";
23
+ console.log("✅ STOMP Connected:", frame.headers);
24
+ // 断线重连后自动恢复之前的订阅
25
+ activeSubscriptions.forEach((record, id) => {
26
+ record.stompSub = client.subscribe(record.topic, (message) => {
27
+ handleMessage(record.topic, message, record.callback);
28
+ });
29
+ });
30
+ };
31
+ client.onStompError = (frame) => {
32
+ connectionStatus.value = "error";
33
+ // console.error("❌ Broker Error:", frame.body);
34
+ };
35
+ client.onDisconnect = () => {
36
+ connectionStatus.value = "disconnected";
37
+ // 不要 clear() 订阅记录,只把底层的 stompSub 实例置空,以便重连后恢复
38
+ activeSubscriptions.forEach((record) => {
39
+ record.stompSub = undefined;
40
+ });
41
+ };
42
+ client.onWebSocketError = (event) => {
43
+ connectionStatus.value = "error";
44
+ // console.error("❌ WebSocket Error:", event);
45
+ };
46
+ stompClient.value = client;
47
+ client.activate();
48
+ };
49
+ // 统一的消息处理函数
50
+ const handleMessage = (topic, message, callback) => {
51
+ try {
52
+ const payload = JSON.parse(message.body);
53
+ callback(payload);
54
+ }
55
+ catch (e) {
56
+ console.error("❌ 错误:", e);
57
+ }
58
+ };
59
+ // 断开
60
+ const disconnect = () => {
61
+ if (stompClient.value?.active) {
62
+ stompClient.value.deactivate();
63
+ connectionStatus.value = "disconnected";
64
+ }
65
+ };
66
+ // 订阅 (返回 topic 以便手动取消)
67
+ const subscribe = (topic, callback) => {
68
+ const record = { topic, callback };
69
+ // 如果当前已连接,直接底层订阅
70
+ if (stompClient.value?.connected) {
71
+ record.stompSub = stompClient.value.subscribe(topic, (message) => {
72
+ handleMessage(topic, message, callback);
73
+ });
74
+ }
75
+ // 将订阅记录存入 Map,使用 topic 作为 key
76
+ activeSubscriptions.set(topic, record);
77
+ return topic;
78
+ };
79
+ // 取消订阅
80
+ const unsubscribe = (topic) => {
81
+ const record = activeSubscriptions.get(topic);
82
+ if (record) {
83
+ record.stompSub?.unsubscribe();
84
+ activeSubscriptions.delete(topic);
85
+ }
86
+ };
87
+ // 发送消息
88
+ const send = (destination, body) => {
89
+ if (stompClient.value?.connected) {
90
+ stompClient.value.publish({
91
+ destination,
92
+ body: JSON.stringify(body),
93
+ headers: { "content-type": "application/json" },
94
+ });
95
+ }
96
+ else {
97
+ // console.warn("STOMP 未连接,无法发送消息");
98
+ }
99
+ };
100
+ return {
101
+ connectionStatus,
102
+ messages,
103
+ connect,
104
+ disconnect,
105
+ subscribe,
106
+ unsubscribe,
107
+ send,
108
+ isConnected: () => stompClient.value?.connected ?? false,
109
+ };
110
+ }
@@ -0,0 +1,21 @@
1
+ export interface SendConfig {
2
+ destination: string;
3
+ interval?: number;
4
+ payload?: () => any;
5
+ }
6
+ export interface StompChannelOptions {
7
+ subscribeTopic?: string;
8
+ sends?: SendConfig[];
9
+ onMessage?: (payload: any) => void;
10
+ onConnected?: (sendFn: any) => void;
11
+ }
12
+ export declare function useStompChannel(stompInstance: any, options?: StompChannelOptions): {
13
+ subscriptionId: import("vue").Ref<string | null, string | null>;
14
+ isConnected: import("vue").Ref<boolean, boolean>;
15
+ lastMessage: import("vue").Ref<any, any>;
16
+ send: (destination: string, payload?: any) => boolean;
17
+ startAllSending: () => void;
18
+ stopAllSending: () => void;
19
+ unsubscribe: () => void;
20
+ };
21
+ //# sourceMappingURL=useStompChannel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStompChannel.d.ts","sourceRoot":"","sources":["../useStompChannel.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CACrC;AAED,wBAAgB,eAAe,CAC7B,aAAa,EAAE,GAAG,EAClB,OAAO,CAAC,EAAE,mBAAmB;;;;wBA+DF,MAAM,YAAY,GAAG;;;;EA6DjD"}
@@ -0,0 +1,108 @@
1
+ import { onUnmounted, ref, watch } from "vue";
2
+ export function useStompChannel(stompInstance, options) {
3
+ const { subscribeTopic, sends, onMessage } = options || {};
4
+ const subscriptionId = ref(null);
5
+ const isConnected = ref(false);
6
+ const lastMessage = ref(null);
7
+ const sendTimers = [];
8
+ // 订阅主题
9
+ const startSubscription = () => {
10
+ console.log("开始订阅:", subscribeTopic);
11
+ const id = stompInstance?.subscribe(subscribeTopic, (payload) => {
12
+ lastMessage.value = payload;
13
+ onMessage?.(payload);
14
+ });
15
+ if (id) {
16
+ console.log("订阅成功:", id);
17
+ subscriptionId.value = id;
18
+ }
19
+ };
20
+ // 开始所有轮询发送
21
+ const startAllSending = () => {
22
+ stopAllSending();
23
+ sends?.forEach((config, index) => {
24
+ const { destination, interval = 3000, payload = () => ({}) } = config;
25
+ // 立即执行一次
26
+ if (stompInstance?.connectionStatus?.value === "connected") {
27
+ stompInstance?.send(destination, {
28
+ ...payload(),
29
+ timestamp: new Date().toISOString(),
30
+ });
31
+ }
32
+ // 然后设置定时器
33
+ const timer = setInterval(() => {
34
+ if (stompInstance?.connectionStatus?.value === "connected") {
35
+ stompInstance?.send(destination, {
36
+ ...payload(),
37
+ timestamp: new Date().toISOString(),
38
+ });
39
+ }
40
+ }, interval);
41
+ sendTimers[index] = timer;
42
+ });
43
+ };
44
+ // 停止所有轮询发送
45
+ const stopAllSending = () => {
46
+ sendTimers.forEach((timer) => {
47
+ if (timer) {
48
+ clearInterval(timer);
49
+ }
50
+ });
51
+ sendTimers.length = 0;
52
+ };
53
+ // 手动发送消息
54
+ const send = (destination, payload) => {
55
+ if (stompInstance?.connectionStatus?.value === "connected") {
56
+ stompInstance?.send(destination, {
57
+ ...payload,
58
+ timestamp: new Date().toISOString(),
59
+ });
60
+ return true;
61
+ }
62
+ return false;
63
+ };
64
+ // 取消订阅
65
+ const unsubscribe = () => {
66
+ if (subscriptionId.value) {
67
+ console.log("取消订阅:", subscriptionId.value);
68
+ stompInstance?.unsubscribe(subscriptionId.value);
69
+ subscriptionId.value = null;
70
+ }
71
+ };
72
+ // 监听连接状态
73
+ const stopWatch = watch(() => stompInstance?.connectionStatus?.value, (status) => {
74
+ isConnected.value = status === "connected";
75
+ if (status === "connected") {
76
+ // 连接成功后开始订阅(如果还没订阅)
77
+ if (subscribeTopic && !subscriptionId.value) {
78
+ startSubscription();
79
+ }
80
+ startAllSending();
81
+ options?.onConnected?.(send);
82
+ }
83
+ else {
84
+ console.log("STOMP 断开连接");
85
+ stopAllSending();
86
+ // 断开时清空订阅ID,下次连接时重新订阅
87
+ if (subscriptionId.value) {
88
+ stompInstance?.unsubscribe(subscriptionId.value);
89
+ }
90
+ subscriptionId.value = null;
91
+ }
92
+ }, { immediate: true });
93
+ // 组件卸载时清理
94
+ onUnmounted(() => {
95
+ stopWatch();
96
+ stopAllSending();
97
+ unsubscribe();
98
+ });
99
+ return {
100
+ subscriptionId,
101
+ isConnected,
102
+ lastMessage,
103
+ send,
104
+ startAllSending,
105
+ stopAllSending,
106
+ unsubscribe,
107
+ };
108
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@omni-vue/stomp-websocket",
3
+ "version": "1.0.0",
4
+ "description": "A Vue 3 STOMP WebSocket library with composable hooks",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "stomp",
17
+ "websocket",
18
+ "vue",
19
+ "vue3",
20
+ "composable",
21
+ "vue-composable",
22
+ "real-time",
23
+ "messaging"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": ""
30
+ },
31
+ "bugs": {
32
+ "url": ""
33
+ },
34
+ "homepage": "",
35
+ "peerDependencies": {
36
+ "@stomp/stompjs": "^7.0.0",
37
+ "vue": "^3.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@stomp/stompjs": "^7.0.0",
41
+ "typescript": "^5.0.0",
42
+ "vue": "^3.0.0"
43
+ }
44
+ }
package/readme.md ADDED
@@ -0,0 +1,229 @@
1
+ # @lejurobot/stomp-websocket
2
+
3
+ A Vue 3 STOMP WebSocket library with composable hooks for real-time messaging.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @lejurobot/stomp-websocket
9
+ # or
10
+ pnpm add @lejurobot/stomp-websocket
11
+ ```
12
+
13
+ ## Peer Dependencies
14
+
15
+ Make sure you have these installed:
16
+
17
+ ```bash
18
+ npm install vue @stomp/stompjs
19
+ ```
20
+
21
+ ## What This Package Provides
22
+
23
+ This package provides two core composable functions:
24
+
25
+ ### 1. `useStomp` - Core STOMP Connection Management
26
+
27
+ Manages WebSocket connection, subscriptions, and message sending.
28
+
29
+ **API:**
30
+
31
+ | Property/Method | Type | Description |
32
+ |----------------|------|-------------|
33
+ | `connectionStatus` | `Ref<ConnectionStatus>` | Connection state: `disconnected` \| `connecting` \| `connected` \| `error` |
34
+ | `messages` | `Ref<any[]>` | Message history (max 50 messages) |
35
+ | `connect` | `() => void` | Establish connection |
36
+ | `disconnect` | `() => void` | Close connection |
37
+ | `subscribe` | `(topic: string, callback: Function) => string` | Subscribe to a topic, returns topic ID |
38
+ | `unsubscribe` | `(topic: string) => void` | Unsubscribe from a topic |
39
+ | `send` | `(destination: string, body: object) => void` | Send a message |
40
+ | `isConnected` | `() => boolean` | Check if connected |
41
+
42
+ ### 2. `useStompChannel` - High-Level Channel Abstraction
43
+
44
+ Encapsulates channel-level polling and subscription with automatic lifecycle management.
45
+
46
+ **API:**
47
+
48
+ | Property/Method | Type | Description |
49
+ |----------------|------|-------------|
50
+ | `isConnected` | `Ref<boolean>` | Connection status |
51
+ | `lastMessage` | `Ref<any>` | Last received message |
52
+ | `send` | `(destination: string, payload?: any) => boolean` | Send a message manually |
53
+ | `startAllSending` | `() => void` | Start all polling timers |
54
+ | `stopAllSending` | `() => void` | Stop all polling timers |
55
+ | `unsubscribe` | `() => void` | Unsubscribe from topic |
56
+
57
+ ## Usage Examples
58
+
59
+ ### Basic Connection
60
+
61
+ ```typescript
62
+ import { useStomp } from '@lejurobot/stomp-websocket'
63
+
64
+ const stomp = useStomp('wss://your-server.com/ws')
65
+
66
+ // Connect
67
+ stomp.connect()
68
+
69
+ // Check connection status
70
+ console.log(stomp.connectionStatus.value) // 'connected'
71
+ ```
72
+
73
+ ### Subscribe to Multiple Topics
74
+
75
+ ```typescript
76
+ import { useStomp } from '@lejurobot/stomp-websocket'
77
+
78
+ const stomp = useStomp('wss://your-server.com/ws')
79
+ stomp.connect()
80
+
81
+ // Subscribe to topic 1
82
+ stomp.subscribe('/topic/notifications', (message) => {
83
+ console.log('Notification:', message)
84
+ })
85
+
86
+ // Subscribe to topic 2
87
+ stomp.subscribe('/topic/updates', (message) => {
88
+ console.log('Update:', message)
89
+ })
90
+
91
+ // Subscribe to topic 3
92
+ stomp.subscribe('/user/queue/messages', (message) => {
93
+ console.log('Private message:', message)
94
+ })
95
+
96
+ // Unsubscribe from a specific topic
97
+ stomp.unsubscribe('/topic/notifications')
98
+ ```
99
+
100
+ ### Send Messages
101
+
102
+ ```typescript
103
+ // Send to destination
104
+ stomp.send('/app/chat', {
105
+ content: 'Hello World',
106
+ timestamp: new Date().toISOString()
107
+ })
108
+ ```
109
+
110
+ ### Channel with Polling
111
+
112
+ ```typescript
113
+ import { useStompChannel } from '@lejurobot/stomp-websocket'
114
+
115
+ // Assuming you have a stomp instance from useStomp
116
+ const stomp = useStomp('wss://your-server.com/ws')
117
+ stomp.connect()
118
+
119
+ const channel = useStompChannel(stomp, {
120
+ subscribeTopic: '/user/queue/data',
121
+ sends: [
122
+ {
123
+ destination: '/app/request-data',
124
+ interval: 3000, // Poll every 3 seconds
125
+ payload: () => ({ page: 1, size: 10 })
126
+ }
127
+ ],
128
+ onMessage: (payload) => {
129
+ console.log('Received:', payload)
130
+ }
131
+ })
132
+ ```
133
+
134
+ ### Multiple Polling Destinations
135
+
136
+ ```typescript
137
+ const channel = useStompChannel(stomp, {
138
+ subscribeTopic: '/user/queue/responses',
139
+ sends: [
140
+ {
141
+ destination: '/app/device-status',
142
+ interval: 2000,
143
+ payload: () => ({ deviceId: '001' })
144
+ },
145
+ {
146
+ destination: '/app/alarms',
147
+ interval: 5000,
148
+ payload: () => ({ level: 'high' })
149
+ }
150
+ ],
151
+ onMessage: (payload) => {
152
+ // Handle all messages from the subscribed topic
153
+ console.log('Message:', payload)
154
+ }
155
+ })
156
+ ```
157
+
158
+ ### Integration with Vue Component
159
+
160
+ Create your own provider component:
161
+
162
+ ```vue
163
+ <template>
164
+ <slot />
165
+ </template>
166
+
167
+ <script setup lang="ts">
168
+ import { useStomp } from '@lejurobot/stomp-websocket'
169
+ import { useWebsocketStore } from '@/stores/websocket'
170
+ import { storeToRefs } from 'pinia'
171
+
172
+ const wsStore = useWebsocketStore()
173
+ const { finalWsUrl } = storeToRefs(wsStore)
174
+
175
+ const stomp = useStomp(finalWsUrl)
176
+ stomp.connect()
177
+
178
+ // Auto-reconnect on URL change
179
+ watch(finalWsUrl, (newUrl, oldUrl) => {
180
+ if (newUrl !== oldUrl && oldUrl !== undefined) {
181
+ stomp.disconnect()
182
+ setTimeout(() => stomp.connect(), 100)
183
+ }
184
+ })
185
+
186
+ onUnmounted(() => {
187
+ stomp.disconnect()
188
+ })
189
+
190
+ // Inject for child components
191
+ provide('STOMP_INSTANCE', stomp)
192
+ </script>
193
+ ```
194
+
195
+ Use in child components:
196
+
197
+ ```vue
198
+ <script setup lang="ts">
199
+ const stomp = inject<any>('STOMP_INSTANCE')
200
+
201
+ // Subscribe to multiple topics
202
+ const topic1 = stomp.subscribe('/topic/news', (msg) => {
203
+ console.log('News:', msg)
204
+ })
205
+
206
+ const topic2 = stomp.subscribe('/topic/alerts', (msg) => {
207
+ console.log('Alert:', msg)
208
+ })
209
+
210
+ onUnmounted(() => {
211
+ stomp.unsubscribe(topic1)
212
+ stomp.unsubscribe(topic2)
213
+ })
214
+ </script>
215
+ ```
216
+
217
+ ## Features
218
+
219
+ - ✅ **Auto Reconnection** - Automatically reconnects on disconnection with configurable delay
220
+ - ✅ **Subscription Recovery** - Automatically restores subscriptions after reconnection
221
+ - ✅ **Multiple Subscriptions** - Subscribe to unlimited topics
222
+ - ✅ **Polling Support** - Built-in interval-based message sending
223
+ - ✅ **TypeScript** - Full TypeScript support with type definitions
224
+ - ✅ **Vue 3 Composable** - Designed for Vue 3 composition API
225
+ - ✅ **Lifecycle Management** - Automatic cleanup on component unmount
226
+
227
+ ## License
228
+
229
+ MIT