1+ import type { ErrorMessage , InferOutput , ObjectEntries , ObjectIssue , ObjectSchema } from 'valibot' ;
12import {
23 any ,
34 array ,
5+ findItem ,
46 integer ,
57 literal ,
68 minValue ,
@@ -11,28 +13,16 @@ import {
1113 pipe ,
1214 safeParse ,
1315 string ,
16+ transform ,
1417 undefinedable ,
1518 union
1619} from 'valibot' ;
17- import type { InferOutput } from 'valibot' ;
1820
1921import { type WebChatActivity } from '../types/WebChatActivity' ;
2022import getOrgSchemaMessage from './getOrgSchemaMessage' ;
2123
2224const EMPTY_ARRAY = Object . freeze ( [ ] ) ;
2325
24- /**
25- * Represents streaming data fields in a streaming Activity
26- */
27- interface StreamingData {
28- /** The unique ID of the stream */
29- streamId ?: string ;
30- /** The sequence number of the streaming message */
31- streamSequence ?: number ;
32- /** The type of streaming message (streaming, informative, or final) */
33- streamType : string ;
34- }
35-
3626const streamSequenceSchema = pipe ( number ( ) , integer ( ) , minValue ( 1 ) ) ;
3727
3828// Fields required for every activity
@@ -46,104 +36,93 @@ const activityFieldsSchema = {
4636// Final Activities have different requirements for "text" fields
4737const { text : _text , ...activityFieldsFinalSchema } = activityFieldsSchema ;
4838
49- const channelDataStreamingActivitySchema = union ( [
39+ function eitherChannelDataOrEntities <
40+ TActivityEntries extends ObjectEntries ,
41+ TActivityMessage extends ErrorMessage < ObjectIssue > | undefined ,
42+ TMetadataEntries extends ObjectEntries ,
43+ TMetadataMessage extends ErrorMessage < ObjectIssue > | undefined
44+ > (
45+ activitySchema : ObjectSchema < TActivityEntries , TActivityMessage > ,
46+ metadataSchema : ObjectSchema < TMetadataEntries , TMetadataMessage >
47+ // metadataSchema: TMetadataSchema
48+ ) {
49+ const metadataInEntitiesSchema = object ( {
50+ ...metadataSchema . entries ,
51+ type : literal ( 'streaminfo' )
52+ } ) ;
53+
54+ function isStreamInfoEntity ( value : unknown ) : value is InferOutput < typeof metadataSchema > {
55+ return safeParse ( metadataInEntitiesSchema , value ) . success ;
56+ }
57+
58+ return union ( [
59+ object ( {
60+ ...activitySchema . entries ,
61+ channelData : metadataSchema
62+ } ) ,
63+ pipe (
64+ object ( {
65+ ...activitySchema . entries ,
66+ // We use `findItem`/`filterItem` than `variant`/`someItem` because the output of the latter is an union type.
67+ // Consider `{ type: string } | { streamId: string; type: 'streaminfo' }`, it turns into `{ type: string }` immediately.
68+
69+ // TODO: [P2] valibot@1.1.0 did not infer output type for `filterItem()`, only infer for `findItem()`.
70+ // Bump valibot@latest and see if they solved the issue.
71+ entities : pipe ( array ( any ( ) ) , findItem ( isStreamInfoEntity ) )
72+ } ) ,
73+ transform ( ( { entities, ...value } ) => ( { ...value , streamInfoEntity : entities } ) )
74+ )
75+ ] ) ;
76+ }
77+
78+ const livestreamingActivitySchema = union ( [
5079 // Interim or Informative message
5180 // Informative may not have "text", but should have abstract instead (checked later)
52- object ( {
53- type : literal ( 'typing' ) ,
54- channelData : object ( {
55- // "streamId" is optional for the very first activity in the session.
81+ eitherChannelDataOrEntities (
82+ object ( {
83+ type : literal ( 'typing' ) ,
84+ ...activityFieldsSchema
85+ } ) ,
86+ // "streamId" is optional for the very first activity in the session.
87+ object ( {
5688 streamId : optional ( undefinedable ( string ( ) ) ) ,
5789 streamSequence : streamSequenceSchema ,
5890 streamType : union ( [ literal ( 'streaming' ) , literal ( 'informative' ) ] )
59- } ) ,
60- entities : optional ( array ( any ( ) ) , EMPTY_ARRAY ) ,
61- ...activityFieldsSchema
62- } ) ,
91+ } )
92+ ) ,
93+
6394 // Conclude with a message.
64- object ( {
65- // If "text" is empty, it represents "regretting" the livestream.
66- type : literal ( 'message' ) ,
67- channelData : object ( {
95+ eitherChannelDataOrEntities (
96+ object ( {
97+ // If "text" is empty, it represents "regretting" the livestream.
98+ type : literal ( 'message' ) ,
99+ ...activityFieldsSchema
100+ } ) ,
101+ object ( {
68102 // "streamId" is required for the final activity in the session.
69103 // The final activity must not be the sole activity in the session.
70104 streamId : pipe ( string ( ) , nonEmpty ( ) ) ,
71105 streamType : literal ( 'final' )
72- } ) ,
73- entities : optional ( array ( any ( ) ) , EMPTY_ARRAY ) ,
74- ...activityFieldsSchema
75- } ) ,
106+ } )
107+ ) ,
108+
76109 // Conclude without a message.
77- object ( {
78- // If "text" is not set or empty, it represents "regretting" the livestream.
79- type : literal ( 'typing' ) ,
80- channelData : object ( {
110+ eitherChannelDataOrEntities (
111+ object ( {
112+ // If "text" is not set or empty, it represents "regretting" the livestream.
113+ type : literal ( 'typing' ) ,
114+ text : optional ( undefinedable ( literal ( '' ) ) ) ,
115+ ...activityFieldsFinalSchema
116+ } ) ,
117+ object ( {
81118 // "streamId" is required for the final activity in the session.
82119 // The final activity must not be the sole activity in the session.
83120 streamId : pipe ( string ( ) , nonEmpty ( ) ) ,
84121 streamType : literal ( 'final' )
85- } ) ,
86- entities : optional ( array ( any ( ) ) , EMPTY_ARRAY ) ,
87- text : optional ( undefinedable ( literal ( '' ) ) ) ,
88- ...activityFieldsFinalSchema
89- } )
90- ] ) ;
91-
92- const entitiesStreamingActivitySchema = union ( [
93- // Interim or Informative message
94- // Informative may not have "text", but should have abstract instead (checked later)
95- object ( {
96- type : literal ( 'typing' ) ,
97- entities : array (
98- object ( {
99- // "streamId" is optional for the very first activity in the session.
100- streamId : optional ( undefinedable ( string ( ) ) ) ,
101- streamSequence : streamSequenceSchema ,
102- streamType : union ( [ literal ( 'streaming' ) , literal ( 'informative' ) ] ) ,
103- type : literal ( 'streaminfo' )
104- } )
105- ) ,
106- channelData : optional ( any ( ) ) ,
107- ...activityFieldsSchema
108- } ) ,
109- // Conclude with a message.
110- object ( {
111- // If "text" is empty, it represents "regretting" the livestream.
112- type : literal ( 'message' ) ,
113- entities : array (
114- object ( {
115- // "streamId" is required for the final activity in the session.
116- // The final activity must not be the sole activity in the session.
117- streamId : pipe ( string ( ) , nonEmpty ( ) ) ,
118- streamType : literal ( 'final' ) ,
119- type : literal ( 'streaminfo' )
120- } )
121- ) ,
122- channelData : optional ( any ( ) ) ,
123- ...activityFieldsSchema
124- } ) ,
125- // Conclude without a message.
126- object ( {
127- // If "text" is empty, it represents "regretting" the livestream.
128- type : literal ( 'typing' ) ,
129- entities : array (
130- object ( {
131- // "streamId" is required for the final activity in the session.
132- // The final activity must not be the sole activity in the session.
133- streamId : pipe ( string ( ) , nonEmpty ( ) ) ,
134- streamType : literal ( 'final' ) ,
135- type : literal ( 'streaminfo' )
136- } )
137- ) ,
138- channelData : optional ( any ( ) ) ,
139- text : optional ( undefinedable ( literal ( '' ) ) ) ,
140- ...activityFieldsFinalSchema
141- } )
122+ } )
123+ )
142124] ) ;
143125
144- type EntitiesStreamingActivity = InferOutput < typeof entitiesStreamingActivitySchema > ;
145- type ChannelDataStreamingActivity = InferOutput < typeof channelDataStreamingActivitySchema > ;
146-
147126/**
148127 * Gets the livestreaming metadata of the activity, or `undefined` if the activity is not participating in a livestreaming session.
149128 *
@@ -168,43 +147,32 @@ export default function getActivityLivestreamingMetadata(activity: WebChatActivi
168147 type : 'contentless' | 'final activity' | 'informative message' | 'interim activity' ;
169148 } >
170149 | undefined {
171- let activityData : EntitiesStreamingActivity | ChannelDataStreamingActivity | undefined ;
150+ const result = safeParse ( livestreamingActivitySchema , activity ) ;
172151
173- let streamingData : StreamingData | undefined ;
174-
175- if ( activity . entities ) {
176- const result = safeParse ( entitiesStreamingActivitySchema , activity ) ;
177- activityData = result . success ? result . output : undefined ;
178- streamingData = result . success ? activityData . entities [ 0 ] : undefined ;
179- }
180-
181- if ( ! activityData && activity . channelData ) {
182- const result = safeParse ( channelDataStreamingActivitySchema , activity ) ;
183- activityData = result . success ? result . output : undefined ;
184- streamingData = result . success ? activityData . channelData : undefined ;
185- }
152+ if ( result . success ) {
153+ const { output } = result ;
154+ const livestreamMetadata = 'channelData' in output ? output . channelData : output . streamInfoEntity ;
186155
187- if ( activityData && streamingData ) {
188156 // If the activity is the first in the session, session ID should be the activity ID.
189- const sessionId = streamingData . streamId || activityData . id ;
157+ const sessionId = livestreamMetadata . streamId || output . id ;
190158
191159 return Object . freeze (
192- streamingData . streamType === 'final'
160+ livestreamMetadata . streamType === 'final'
193161 ? {
194162 sequenceNumber : Infinity ,
195163 sessionId,
196164 type : 'final activity'
197165 }
198166 : {
199- sequenceNumber : streamingData . streamSequence ,
167+ sequenceNumber : livestreamMetadata . streamSequence ,
200168 sessionId,
201169 type : ! (
202- activityData . text ||
203- activityData . attachments ?. length ||
204- ( 'entities' in activityData && getOrgSchemaMessage ( activity . entities ) ?. abstract )
170+ output . text ||
171+ output . attachments ?. length ||
172+ ( 'entities' in output && getOrgSchemaMessage ( activity . entities ) ?. abstract )
205173 )
206174 ? 'contentless'
207- : streamingData . streamType === 'informative'
175+ : livestreamMetadata . streamType === 'informative'
208176 ? 'informative message'
209177 : 'interim activity'
210178 }
0 commit comments