Skip to content

Commit a7a1665

Browse files
committed
Use valibot properly
1 parent b950823 commit a7a1665

1 file changed

Lines changed: 84 additions & 116 deletions

File tree

packages/core/src/utils/getActivityLivestreamingMetadata.ts

Lines changed: 84 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { ErrorMessage, InferOutput, ObjectEntries, ObjectIssue, ObjectSchema } from 'valibot';
12
import {
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

1921
import { type WebChatActivity } from '../types/WebChatActivity';
2022
import getOrgSchemaMessage from './getOrgSchemaMessage';
2123

2224
const 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-
3626
const 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
4737
const { 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

Comments
 (0)