@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.
Files changed (30) hide show
  1. package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
  2. package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
  3. package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
  4. package/dist/components/AskUserFormCard.d.ts.map +1 -1
  5. package/dist/index.js +1624 -1382
  6. package/dist/runtime/buildFormRuntimeSnapshot.d.ts +16 -0
  7. package/dist/runtime/buildFormRuntimeSnapshot.d.ts.map +1 -0
  8. package/dist/runtime/formLiveOptionsContext.d.ts +20 -0
  9. package/dist/runtime/formLiveOptionsContext.d.ts.map +1 -0
  10. package/dist/runtime/formRuntimeConnection.d.ts +35 -0
  11. package/dist/runtime/formRuntimeConnection.d.ts.map +1 -0
  12. package/dist/runtime/formRuntimeContext.d.ts +2 -2
  13. package/dist/runtime/formRuntimeContext.d.ts.map +1 -1
  14. package/dist/runtime/formRuntimeWs.d.ts +8 -13
  15. package/dist/runtime/formRuntimeWs.d.ts.map +1 -1
  16. package/dist/runtime/types.d.ts +11 -0
  17. package/dist/runtime/types.d.ts.map +1 -1
  18. package/package.json +1 -1
  19. package/src/catalog/skoponCaseSelect.tsx +12 -1
  20. package/src/catalog/skoponResumeSelect.tsx +12 -1
  21. package/src/catalog/useSkoponBoundField.ts +21 -0
  22. package/src/components/AskUserFormCard.tsx +141 -31
  23. package/src/runtime/buildFormRuntimeSnapshot.test.ts +97 -0
  24. package/src/runtime/buildFormRuntimeSnapshot.ts +61 -0
  25. package/src/runtime/formLiveOptionsContext.tsx +118 -0
  26. package/src/runtime/formRuntimeConnection.test.ts +155 -0
  27. package/src/runtime/formRuntimeConnection.ts +251 -0
  28. package/src/runtime/formRuntimeContext.tsx +13 -11
  29. package/src/runtime/formRuntimeWs.ts +24 -129
  30. 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: (values: Record<string, unknown>) => void
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 omitUndefinedValues(
41
- values: Record<string, unknown>,
42
- ): Record<string, unknown> {
43
- const out: Record<string, unknown> = {}
44
- for (const [key, value] of Object.entries(values)) {
45
- if (value !== undefined) out[key] = value
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((values: Record<string, unknown>) => {
81
+ const reportData = useCallback((payload: FormRuntimeDataPayload) => {
80
82
  const ws = clientRef.current
81
83
  if (!ws) return
82
- ws.send('form_runtime_data', omitUndefinedValues(values))
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 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,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 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
- 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
- const payload: FormRuntimeEnvelope = {
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.clearReconnectTimer()
124
- const delay = Math.min(
125
- RECONNECT_MAX_MS,
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
  }
@@ -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
+ }