@skopon-cool/form-sdk 0.3.1 → 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.
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/index.js +1624 -1382
- 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 -13
- 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/catalog/useSkoponBoundField.ts +21 -0
- package/src/components/AskUserFormCard.tsx +141 -31
- 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 -129
- package/src/runtime/types.ts +12 -0
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from 'react'
|
|
9
9
|
import type { FormRuntimeWsClient } from './formRuntimeWs'
|
|
10
|
-
import type { FormRuntimeEventData } from './types'
|
|
10
|
+
import type { FormRuntimeDataPayload, FormRuntimeEventData } from './types'
|
|
11
11
|
|
|
12
12
|
export interface FormRuntimeContextValue {
|
|
13
13
|
reportEvent: (event: FormRuntimeEventData) => void
|
|
14
|
-
reportData: (
|
|
14
|
+
reportData: (payload: FormRuntimeDataPayload) => void
|
|
15
15
|
/**
|
|
16
16
|
* 在自动控上下文中执行 fn。若 fn 返回 Promise,depth 保持到 settle,
|
|
17
17
|
* 使异步 submit 内的 reportEvent 仍为 form_auto_control_bac_event。
|
|
@@ -37,14 +37,16 @@ const noopRuntime: FormRuntimeContextValue = {
|
|
|
37
37
|
isAutoControl: () => false,
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
function sanitizePayload(payload: FormRuntimeDataPayload): FormRuntimeDataPayload {
|
|
41
|
+
const form_values: Record<string, unknown> = {}
|
|
42
|
+
for (const [key, value] of Object.entries(payload.form_values ?? {})) {
|
|
43
|
+
if (value !== undefined) form_values[key] = value
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
form_schema: payload.form_schema,
|
|
47
|
+
form_values,
|
|
48
|
+
form_change: Array.isArray(payload.form_change) ? [...payload.form_change] : [],
|
|
46
49
|
}
|
|
47
|
-
return out
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface FormRuntimeProviderProps {
|
|
@@ -76,10 +78,10 @@ export function FormRuntimeProvider({
|
|
|
76
78
|
ws.send(type, data)
|
|
77
79
|
}, [])
|
|
78
80
|
|
|
79
|
-
const reportData = useCallback((
|
|
81
|
+
const reportData = useCallback((payload: FormRuntimeDataPayload) => {
|
|
80
82
|
const ws = clientRef.current
|
|
81
83
|
if (!ws) return
|
|
82
|
-
ws.send('form_runtime_data',
|
|
84
|
+
ws.send('form_runtime_data', sanitizePayload(payload) as unknown as Record<string, unknown>)
|
|
83
85
|
}, [])
|
|
84
86
|
|
|
85
87
|
const runAutoControl = useCallback(<T,>(fn: () => T): T => {
|
|
@@ -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,46 +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
|
-
try {
|
|
41
|
-
const ws = new WebSocket(this.url)
|
|
42
|
-
this.ws = ws
|
|
43
|
-
ws.onopen = () => {
|
|
44
|
-
this.reconnectAttempt = 0
|
|
45
|
-
this.flushQueue()
|
|
46
|
-
}
|
|
47
|
-
ws.onmessage = (event) => {
|
|
48
|
-
this.handleMessage(event.data)
|
|
49
|
-
}
|
|
50
|
-
ws.onclose = () => {
|
|
51
|
-
this.ws = null
|
|
52
|
-
this.scheduleReconnect()
|
|
53
|
-
}
|
|
54
|
-
ws.onerror = () => {
|
|
55
|
-
// onclose will follow; avoid noisy logs
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.warn('[FormRuntimeWs] connect failed', error)
|
|
59
|
-
this.scheduleReconnect()
|
|
60
|
-
}
|
|
35
|
+
if (this.closed) return
|
|
36
|
+
this.connection.connect()
|
|
61
37
|
}
|
|
62
38
|
|
|
63
39
|
send(
|
|
@@ -65,94 +41,13 @@ export class FormRuntimeWsClient {
|
|
|
65
41
|
data: Record<string, unknown>,
|
|
66
42
|
): void {
|
|
67
43
|
if (this.closed) return
|
|
68
|
-
|
|
69
|
-
type,
|
|
70
|
-
ask_user_message_id: this.askUserMessageId,
|
|
71
|
-
data,
|
|
72
|
-
}
|
|
73
|
-
let serialized: string
|
|
74
|
-
try {
|
|
75
|
-
serialized = JSON.stringify(payload)
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.warn('[FormRuntimeWs] serialize failed', error)
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
81
|
-
this.ws.send(serialized)
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
this.enqueue(serialized)
|
|
44
|
+
this.connection.send(this.askUserMessageId, type, data)
|
|
85
45
|
}
|
|
86
46
|
|
|
87
47
|
close(): void {
|
|
88
|
-
this.closed = true
|
|
89
|
-
this.clearReconnectTimer()
|
|
90
|
-
this.sendQueue = []
|
|
91
|
-
if (this.ws) {
|
|
92
|
-
this.ws.onopen = null
|
|
93
|
-
this.ws.onmessage = null
|
|
94
|
-
this.ws.onclose = null
|
|
95
|
-
this.ws.onerror = null
|
|
96
|
-
try {
|
|
97
|
-
this.ws.close()
|
|
98
|
-
} catch {
|
|
99
|
-
// ignore
|
|
100
|
-
}
|
|
101
|
-
this.ws = null
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private enqueue(serialized: string): void {
|
|
106
|
-
this.sendQueue.push(serialized)
|
|
107
|
-
while (this.sendQueue.length > SEND_QUEUE_MAX) {
|
|
108
|
-
this.sendQueue.shift()
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private flushQueue(): void {
|
|
113
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return
|
|
114
|
-
const pending = this.sendQueue
|
|
115
|
-
this.sendQueue = []
|
|
116
|
-
for (const item of pending) {
|
|
117
|
-
this.ws.send(item)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private scheduleReconnect(): void {
|
|
122
48
|
if (this.closed) return
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
1000 * 2 ** Math.min(this.reconnectAttempt, 5),
|
|
127
|
-
)
|
|
128
|
-
this.reconnectAttempt += 1
|
|
129
|
-
this.reconnectTimer = setTimeout(() => {
|
|
130
|
-
this.reconnectTimer = null
|
|
131
|
-
this.connect()
|
|
132
|
-
}, delay)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private clearReconnectTimer(): void {
|
|
136
|
-
if (this.reconnectTimer != null) {
|
|
137
|
-
clearTimeout(this.reconnectTimer)
|
|
138
|
-
this.reconnectTimer = null
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private handleMessage(raw: unknown): void {
|
|
143
|
-
if (typeof raw !== 'string') return
|
|
144
|
-
let parsed: unknown
|
|
145
|
-
try {
|
|
146
|
-
parsed = JSON.parse(raw)
|
|
147
|
-
} catch {
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return
|
|
151
|
-
const envelope = parsed as FormRuntimeEnvelope<FormAutoControlEventData>
|
|
152
|
-
if (envelope.type !== 'form_auto_control_event') return
|
|
153
|
-
if (String(envelope.ask_user_message_id ?? '') !== this.askUserMessageId) return
|
|
154
|
-
const data = envelope.data
|
|
155
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) return
|
|
156
|
-
this.onAutoControl?.(data, envelope)
|
|
49
|
+
this.closed = true
|
|
50
|
+
this.unsubscribe()
|
|
51
|
+
this.connection.release()
|
|
157
52
|
}
|
|
158
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
|
+
}
|