@skopon-cool/form-sdk 0.3.2 → 0.4.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.
@@ -1,16 +1,11 @@
1
- import type {
2
- FormAutoControlEventData,
3
- FormRuntimeEnvelope,
4
- FormRuntimeOutboundType,
5
- } from './types'
1
+ import {
2
+ acquireFormRuntimeConnection,
3
+ type FormAutoControlHandler,
4
+ type FormRuntimeConnection,
5
+ } from './formRuntimeConnection'
6
+ import type { FormRuntimeOutboundType } from './types'
6
7
 
7
- const SEND_QUEUE_MAX = 100
8
- const RECONNECT_MAX_MS = 30_000
9
-
10
- export type FormAutoControlHandler = (
11
- data: FormAutoControlEventData,
12
- envelope: FormRuntimeEnvelope<FormAutoControlEventData>,
13
- ) => void
8
+ export type { FormAutoControlHandler } from './formRuntimeConnection'
14
9
 
15
10
  export interface FormRuntimeWsClientOptions {
16
11
  url: string
@@ -18,47 +13,27 @@ export interface FormRuntimeWsClientOptions {
18
13
  onAutoControl?: FormAutoControlHandler
19
14
  }
20
15
 
16
+ /**
17
+ * Per-card runtime 会话:绑定 ask_user_message_id,代理到按 URL 的全局单例连接。
18
+ */
21
19
  export class FormRuntimeWsClient {
22
- private readonly url: string
23
20
  private readonly askUserMessageId: string
24
- private readonly onAutoControl?: FormAutoControlHandler
25
- private ws: WebSocket | null = null
21
+ private readonly connection: FormRuntimeConnection
22
+ private readonly unsubscribe: () => void
26
23
  private closed = false
27
- private reconnectAttempt = 0
28
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null
29
- private sendQueue: string[] = []
30
24
 
31
25
  constructor(options: FormRuntimeWsClientOptions) {
32
- this.url = options.url
33
- this.askUserMessageId = options.askUserMessageId
34
- this.onAutoControl = options.onAutoControl
26
+ this.askUserMessageId = options.askUserMessageId.trim()
27
+ this.connection = acquireFormRuntimeConnection(options.url)
28
+ const handler = options.onAutoControl
29
+ this.unsubscribe = handler
30
+ ? this.connection.subscribe(this.askUserMessageId, handler)
31
+ : () => {}
35
32
  }
36
33
 
37
34
  connect(): void {
38
- if (this.closed || typeof WebSocket === 'undefined') return
39
- this.clearReconnectTimer()
40
- this.disposeSocket()
41
- try {
42
- const ws = new WebSocket(this.url)
43
- this.ws = ws
44
- ws.onopen = () => {
45
- this.reconnectAttempt = 0
46
- this.flushQueue()
47
- }
48
- ws.onmessage = (event) => {
49
- this.handleMessage(event.data)
50
- }
51
- ws.onclose = () => {
52
- if (this.ws === ws) this.ws = null
53
- this.scheduleReconnect()
54
- }
55
- ws.onerror = () => {
56
- // onclose will follow; avoid noisy logs
57
- }
58
- } catch (error) {
59
- console.warn('[FormRuntimeWs] connect failed', error)
60
- this.scheduleReconnect()
61
- }
35
+ if (this.closed) return
36
+ this.connection.connect()
62
37
  }
63
38
 
64
39
  send(
@@ -66,98 +41,13 @@ export class FormRuntimeWsClient {
66
41
  data: Record<string, unknown>,
67
42
  ): void {
68
43
  if (this.closed) return
69
- const payload: FormRuntimeEnvelope = {
70
- type,
71
- ask_user_message_id: this.askUserMessageId,
72
- data,
73
- }
74
- let serialized: string
75
- try {
76
- serialized = JSON.stringify(payload)
77
- } catch (error) {
78
- console.warn('[FormRuntimeWs] serialize failed', error)
79
- return
80
- }
81
- if (this.ws?.readyState === WebSocket.OPEN) {
82
- this.ws.send(serialized)
83
- return
84
- }
85
- this.enqueue(serialized)
44
+ this.connection.send(this.askUserMessageId, type, data)
86
45
  }
87
46
 
88
47
  close(): void {
89
- this.closed = true
90
- this.clearReconnectTimer()
91
- this.sendQueue = []
92
- this.disposeSocket()
93
- }
94
-
95
- /** 关闭当前 socket 且不触发重连(清掉 onclose),建新连或主动 close 时用。 */
96
- private disposeSocket(): void {
97
- if (!this.ws) return
98
- this.ws.onopen = null
99
- this.ws.onmessage = null
100
- this.ws.onclose = null
101
- this.ws.onerror = null
102
- try {
103
- this.ws.close()
104
- } catch {
105
- // ignore
106
- }
107
- this.ws = null
108
- }
109
-
110
- private enqueue(serialized: string): void {
111
- this.sendQueue.push(serialized)
112
- while (this.sendQueue.length > SEND_QUEUE_MAX) {
113
- this.sendQueue.shift()
114
- }
115
- }
116
-
117
- private flushQueue(): void {
118
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return
119
- const pending = this.sendQueue
120
- this.sendQueue = []
121
- for (const item of pending) {
122
- this.ws.send(item)
123
- }
124
- }
125
-
126
- private scheduleReconnect(): void {
127
48
  if (this.closed) return
128
- this.clearReconnectTimer()
129
- const delay = Math.min(
130
- RECONNECT_MAX_MS,
131
- 1000 * 2 ** Math.min(this.reconnectAttempt, 5),
132
- )
133
- this.reconnectAttempt += 1
134
- this.reconnectTimer = setTimeout(() => {
135
- this.reconnectTimer = null
136
- this.connect()
137
- }, delay)
138
- }
139
-
140
- private clearReconnectTimer(): void {
141
- if (this.reconnectTimer != null) {
142
- clearTimeout(this.reconnectTimer)
143
- this.reconnectTimer = null
144
- }
145
- }
146
-
147
- private handleMessage(raw: unknown): void {
148
- if (typeof raw !== 'string') return
149
- let parsed: unknown
150
- try {
151
- parsed = JSON.parse(raw)
152
- } catch {
153
- return
154
- }
155
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return
156
- const envelope = parsed as FormRuntimeEnvelope<FormAutoControlEventData>
157
- if (envelope.type !== 'form_auto_control_event') return
158
- if (String(envelope.ask_user_message_id ?? '') !== this.askUserMessageId) return
159
- const data = envelope.data
160
- if (!data || typeof data !== 'object' || Array.isArray(data)) return
161
- this.onAutoControl?.(data, envelope)
49
+ this.closed = true
50
+ this.unsubscribe()
51
+ this.connection.release()
162
52
  }
163
53
  }
@@ -24,3 +24,15 @@ export interface FormRuntimeEnvelope<T = Record<string, unknown>> {
24
24
  ask_user_message_id: string
25
25
  data: T
26
26
  }
27
+
28
+ /** form_runtime_data 的 data 载荷(Init / Refresh / Submit)。 */
29
+ export interface FormRuntimeDataPayload {
30
+ form_schema: {
31
+ title?: string
32
+ description?: string
33
+ blocks: unknown[]
34
+ }
35
+ form_values: Record<string, unknown>
36
+ /** 本次被更新的字段名;Init/Submit 为空数组。 */
37
+ form_change: string[]
38
+ }