@skopon-cool/form-sdk 0.3.2 → 0.4.1
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/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/index.js +1386 -1173
- package/dist/runtime/buildFormRuntimeSnapshot.d.ts +16 -0
- package/dist/runtime/buildFormRuntimeSnapshot.d.ts.map +1 -0
- package/dist/runtime/formLiveOptionsContext.d.ts +20 -0
- package/dist/runtime/formLiveOptionsContext.d.ts.map +1 -0
- package/dist/runtime/formRuntimeConnection.d.ts +35 -0
- package/dist/runtime/formRuntimeConnection.d.ts.map +1 -0
- package/dist/runtime/formRuntimeContext.d.ts +2 -2
- package/dist/runtime/formRuntimeContext.d.ts.map +1 -1
- package/dist/runtime/formRuntimeWs.d.ts +8 -15
- package/dist/runtime/formRuntimeWs.d.ts.map +1 -1
- package/dist/runtime/types.d.ts +11 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/catalog/skoponCaseSelect.tsx +12 -1
- package/src/catalog/skoponResumeSelect.tsx +12 -1
- package/src/components/AskUserFormCard.tsx +112 -13
- package/src/runtime/buildFormRuntimeSnapshot.test.ts +97 -0
- package/src/runtime/buildFormRuntimeSnapshot.ts +61 -0
- package/src/runtime/formLiveOptionsContext.tsx +118 -0
- package/src/runtime/formRuntimeConnection.test.ts +155 -0
- package/src/runtime/formRuntimeConnection.ts +251 -0
- package/src/runtime/formRuntimeContext.tsx +13 -11
- package/src/runtime/formRuntimeWs.ts +24 -134
- package/src/runtime/types.ts +12 -0
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from './
|
|
1
|
+
import {
|
|
2
|
+
acquireFormRuntimeConnection,
|
|
3
|
+
type FormAutoControlHandler,
|
|
4
|
+
type FormRuntimeConnection,
|
|
5
|
+
} from './formRuntimeConnection'
|
|
6
|
+
import type { FormRuntimeOutboundType } from './types'
|
|
6
7
|
|
|
7
|
-
|
|
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
|
|
25
|
-
private
|
|
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.
|
|
33
|
-
this.
|
|
34
|
-
|
|
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
|
|
39
|
-
this.
|
|
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
|
-
|
|
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.
|
|
129
|
-
|
|
130
|
-
|
|
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
|
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -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
|
+
}
|