@usereq/widget 0.2.25 → 1.0.0-experimental.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/README.md +35 -145
- package/dist/embed.js +38 -0
- package/dist/embed.js.map +1 -0
- package/dist/embed.mjs +2839 -0
- package/dist/embed.mjs.map +1 -0
- package/package.json +35 -81
- package/dist/widget.js +0 -255
- package/src/chat-widget/chat-widget-appearance.ts +0 -134
- package/src/chat-widget/message-reveal.ts +0 -52
- package/src/chat-widget/stop-confirmation.ts +0 -288
- package/src/custom-element/agent-widget-element.tsx +0 -682
- package/src/embed.ts +0 -8
- package/src/index.ts +0 -9
- package/src/register.ts +0 -15
- package/src/renderer/index.ts +0 -6
- package/src/renderer/widget-runtime.tsx +0 -395
- package/src/runtime/api-origin.ts +0 -29
- package/src/runtime/api.ts +0 -123
- package/src/runtime/bootstrap.ts +0 -366
- package/src/runtime/debug.ts +0 -104
- package/src/runtime/messages.ts +0 -12
- package/src/runtime/session-storage.ts +0 -32
- package/src/runtime/trigger-rule.ts +0 -182
- package/src/shared/shadow-css.ts +0 -15
- package/src/shared/shadow-theme.ts +0 -115
- package/src/shared/stop-confirmation.ts +0 -20
- package/src/shared/widget-config.ts +0 -171
- package/src/styles/widget.css.ts +0 -83
- package/src/types.ts +0 -98
- package/src/vite-env.d.ts +0 -4
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
export type ChatWidgetGradient = {
|
|
4
|
-
from: string;
|
|
5
|
-
to: string;
|
|
6
|
-
angle?: number | null;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
/** Solid hex or gradient. Mirrors `ChatColor` in `@usereq/ui`. */
|
|
10
|
-
export type ChatWidgetColor = string | ChatWidgetGradient;
|
|
11
|
-
|
|
12
|
-
export type ChatWidgetIconPack = "lucide" | "heroicons" | "phosphor";
|
|
13
|
-
|
|
14
|
-
export type ChatWidgetIconSlot =
|
|
15
|
-
| "launcher"
|
|
16
|
-
| "send"
|
|
17
|
-
| "close"
|
|
18
|
-
| "minimize"
|
|
19
|
-
| "attach"
|
|
20
|
-
| "mic";
|
|
21
|
-
|
|
22
|
-
export type ChatWidgetIcons = {
|
|
23
|
-
pack?: ChatWidgetIconPack | null;
|
|
24
|
-
overrides?: Partial<Record<ChatWidgetIconSlot, string>> | null;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Every appearance token the agents row can store.
|
|
29
|
-
*
|
|
30
|
-
* This deliberately mirrors `ChatAppearance` in `@usereq/ui` field-for-field,
|
|
31
|
-
* but is declared standalone rather than imported: `shared/widget-config` is
|
|
32
|
-
* consumed by `apps/api`, which has no `@usereq/ui` dependency. Structural
|
|
33
|
-
* typing means the two still line up where they meet.
|
|
34
|
-
*
|
|
35
|
-
* Keep in sync with `widgetAppearanceSchema` in the API's `widget.model.ts`
|
|
36
|
-
* and `AgentWidgetAppearance` in the dashboard.
|
|
37
|
-
*/
|
|
38
|
-
export type ChatWidgetAppearance = {
|
|
39
|
-
// — Legacy gradient (still the fallback when `primary` is unset) —
|
|
40
|
-
avatarOrbColor1?: string | null;
|
|
41
|
-
avatarOrbColor2?: string | null;
|
|
42
|
-
|
|
43
|
-
// — Brand —
|
|
44
|
-
primary?: ChatWidgetColor | null;
|
|
45
|
-
primaryForeground?: string | null;
|
|
46
|
-
secondary?: string | null;
|
|
47
|
-
secondaryForeground?: string | null;
|
|
48
|
-
actionText?: string | null;
|
|
49
|
-
|
|
50
|
-
// — Surface tokens (panel) —
|
|
51
|
-
background?: string | null;
|
|
52
|
-
foreground?: string | null;
|
|
53
|
-
muted?: string | null;
|
|
54
|
-
mutedForeground?: string | null;
|
|
55
|
-
border?: string | null;
|
|
56
|
-
ring?: string | null;
|
|
57
|
-
|
|
58
|
-
// — Granular text overrides —
|
|
59
|
-
textPrimary?: string | null;
|
|
60
|
-
textForeground?: string | null;
|
|
61
|
-
textMutedForeground?: string | null;
|
|
62
|
-
|
|
63
|
-
// — Icon pack swap (persist-only for now) —
|
|
64
|
-
icons?: ChatWidgetIcons | null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Only the three legacy fields are guaranteed present — every other token
|
|
69
|
-
* stays optional on purpose, because `undefined` means "not customized, use
|
|
70
|
-
* the design-system default". Filling them with defaults here would emit
|
|
71
|
-
* `--chat-*` variables for tokens the user never set and lock the panel out
|
|
72
|
-
* of the host theme.
|
|
73
|
-
*/
|
|
74
|
-
export type ResolvedChatWidgetAppearance = ChatWidgetAppearance & {
|
|
75
|
-
avatarOrbColor1: string;
|
|
76
|
-
avatarOrbColor2: string;
|
|
77
|
-
actionText: string;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const DEFAULT_CHAT_WIDGET_APPEARANCE: ResolvedChatWidgetAppearance = {
|
|
81
|
-
avatarOrbColor1: "#2563eb",
|
|
82
|
-
avatarOrbColor2: "#60a5fa",
|
|
83
|
-
actionText: "Need help?",
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Fills defaults for the three legacy fields and passes every other token
|
|
88
|
-
* through untouched.
|
|
89
|
-
*
|
|
90
|
-
* The spread is load-bearing: this function used to construct a fresh
|
|
91
|
-
* three-key object, which silently discarded the entire saved theme
|
|
92
|
-
* (background, primary, text colors…) on its way to the embed. Any new token
|
|
93
|
-
* added to `ChatWidgetAppearance` now survives without editing this function.
|
|
94
|
-
*/
|
|
95
|
-
export function resolveChatWidgetAppearance(
|
|
96
|
-
appearance?: Partial<ChatWidgetAppearance> | null,
|
|
97
|
-
): ResolvedChatWidgetAppearance {
|
|
98
|
-
return {
|
|
99
|
-
...(appearance ?? {}),
|
|
100
|
-
avatarOrbColor1:
|
|
101
|
-
normalizeString(appearance?.avatarOrbColor1) ??
|
|
102
|
-
DEFAULT_CHAT_WIDGET_APPEARANCE.avatarOrbColor1,
|
|
103
|
-
avatarOrbColor2:
|
|
104
|
-
normalizeString(appearance?.avatarOrbColor2) ??
|
|
105
|
-
DEFAULT_CHAT_WIDGET_APPEARANCE.avatarOrbColor2,
|
|
106
|
-
actionText:
|
|
107
|
-
normalizeString(appearance?.actionText) ??
|
|
108
|
-
DEFAULT_CHAT_WIDGET_APPEARANCE.actionText,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function getChatWidgetGradientStyle(
|
|
113
|
-
appearance?: Partial<ChatWidgetAppearance> | null,
|
|
114
|
-
): CSSProperties {
|
|
115
|
-
const resolved = resolveChatWidgetAppearance(appearance);
|
|
116
|
-
return {
|
|
117
|
-
background: `linear-gradient(135deg, ${resolved.avatarOrbColor1}, ${resolved.avatarOrbColor2})`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function getChatWidgetAvatarStyle(
|
|
122
|
-
appearance?: Partial<ChatWidgetAppearance> | null,
|
|
123
|
-
): CSSProperties {
|
|
124
|
-
const resolved = resolveChatWidgetAppearance(appearance);
|
|
125
|
-
return {
|
|
126
|
-
background: `linear-gradient(135deg, ${resolved.avatarOrbColor1}, ${resolved.avatarOrbColor2})`,
|
|
127
|
-
boxShadow: `0 10px 24px color-mix(in srgb, ${resolved.avatarOrbColor1} 22%, transparent)`,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function normalizeString(value: string | null | undefined): string | null {
|
|
132
|
-
const trimmed = value?.trim();
|
|
133
|
-
return trimmed ? trimmed : null;
|
|
134
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Staggered reveal timing for assistant message turns. Pure helper (no React)
|
|
3
|
-
* used by the custom element to drip assistant replies in one-by-one with a
|
|
4
|
-
* brief "typing" hold between them.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export type AssistantMessageRevealSource = {
|
|
8
|
-
id: string;
|
|
9
|
-
content: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type AssistantMessageRevealStep<
|
|
13
|
-
T extends AssistantMessageRevealSource = AssistantMessageRevealSource,
|
|
14
|
-
> = {
|
|
15
|
-
message: T;
|
|
16
|
-
delayMs: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function buildAssistantMessageRevealSteps<
|
|
20
|
-
T extends AssistantMessageRevealSource,
|
|
21
|
-
>(
|
|
22
|
-
messages: T[],
|
|
23
|
-
options?: {
|
|
24
|
-
initialDelayMs?: number;
|
|
25
|
-
typingHoldMs?: number;
|
|
26
|
-
interMessageDelayMs?: number;
|
|
27
|
-
interMessageDelayJitterMs?: number;
|
|
28
|
-
},
|
|
29
|
-
): AssistantMessageRevealStep<T>[] {
|
|
30
|
-
if (messages.length === 0) {
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const initialDelayMs = options?.initialDelayMs ?? 0;
|
|
35
|
-
const typingHoldMs = options?.typingHoldMs ?? 1260;
|
|
36
|
-
const interMessageDelayMs = options?.interMessageDelayMs ?? 1520;
|
|
37
|
-
const jitterMs = Math.max(0, options?.interMessageDelayJitterMs ?? 100);
|
|
38
|
-
|
|
39
|
-
return messages.map((message, index) => {
|
|
40
|
-
const baseDelay =
|
|
41
|
-
index === 0 ? initialDelayMs + typingHoldMs : interMessageDelayMs;
|
|
42
|
-
const jitter =
|
|
43
|
-
index === 0 || jitterMs === 0
|
|
44
|
-
? 0
|
|
45
|
-
: Math.round((Math.random() * 2 - 1) * jitterMs);
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
message,
|
|
49
|
-
delayMs: Math.max(0, baseDelay + jitter),
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
}
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
export const STOP_CONFIRMATION_KIND = "stop_confirmation" as const;
|
|
2
|
-
export const STOP_CONFIRMATION_RESULT_KIND =
|
|
3
|
-
"stop_confirmation_result" as const;
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_STOP_CONVERSATION_PROMPT =
|
|
6
|
-
"Do you want to stop this conversation?";
|
|
7
|
-
|
|
8
|
-
export type StopConfirmationState = "pending" | "accepted" | "rejected";
|
|
9
|
-
export type StopConfirmationDecision = "accepted" | "rejected";
|
|
10
|
-
|
|
11
|
-
export type StopConfirmationPromptEnvelope = {
|
|
12
|
-
kind: typeof STOP_CONFIRMATION_KIND;
|
|
13
|
-
confirmationId: string;
|
|
14
|
-
state: StopConfirmationState;
|
|
15
|
-
prompt: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type StopConfirmationResultEnvelope = {
|
|
19
|
-
kind: typeof STOP_CONFIRMATION_RESULT_KIND;
|
|
20
|
-
confirmationId: string;
|
|
21
|
-
decision: StopConfirmationDecision;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type StopConfirmationEnvelope =
|
|
25
|
-
| StopConfirmationPromptEnvelope
|
|
26
|
-
| StopConfirmationResultEnvelope;
|
|
27
|
-
|
|
28
|
-
export type StopConfirmationMessageLike = {
|
|
29
|
-
id: string;
|
|
30
|
-
content: string;
|
|
31
|
-
createdAt?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type StopConfirmationRenderItem<T extends StopConfirmationMessageLike> =
|
|
35
|
-
| {
|
|
36
|
-
type: "message";
|
|
37
|
-
message: T;
|
|
38
|
-
}
|
|
39
|
-
| {
|
|
40
|
-
type: "confirmation";
|
|
41
|
-
message: T;
|
|
42
|
-
envelope: StopConfirmationPromptEnvelope;
|
|
43
|
-
latestEnvelope: StopConfirmationEnvelope;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function normalizeState(value: unknown): StopConfirmationState {
|
|
51
|
-
return value === "accepted" || value === "rejected" ? value : "pending";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizeDecision(value: unknown): StopConfirmationDecision | null {
|
|
55
|
-
return value === "accepted" || value === "rejected" ? value : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function normalizePrompt(value: unknown): string {
|
|
59
|
-
if (typeof value !== "string") {
|
|
60
|
-
return DEFAULT_STOP_CONVERSATION_PROMPT;
|
|
61
|
-
}
|
|
62
|
-
const trimmed = value.trim();
|
|
63
|
-
return trimmed.length > 0 ? trimmed : DEFAULT_STOP_CONVERSATION_PROMPT;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function createStopConfirmationPromptEnvelope(input: {
|
|
67
|
-
confirmationId: string;
|
|
68
|
-
prompt?: string;
|
|
69
|
-
state?: StopConfirmationState;
|
|
70
|
-
}): StopConfirmationPromptEnvelope {
|
|
71
|
-
return {
|
|
72
|
-
kind: STOP_CONFIRMATION_KIND,
|
|
73
|
-
confirmationId: input.confirmationId,
|
|
74
|
-
state: normalizeState(input.state),
|
|
75
|
-
prompt: normalizePrompt(input.prompt),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function createStopConfirmationResultEnvelope(input: {
|
|
80
|
-
confirmationId: string;
|
|
81
|
-
decision: StopConfirmationDecision;
|
|
82
|
-
}): StopConfirmationResultEnvelope {
|
|
83
|
-
return {
|
|
84
|
-
kind: STOP_CONFIRMATION_RESULT_KIND,
|
|
85
|
-
confirmationId: input.confirmationId,
|
|
86
|
-
decision: input.decision,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function encodeStopConfirmationEnvelope(
|
|
91
|
-
envelope: StopConfirmationEnvelope,
|
|
92
|
-
): string {
|
|
93
|
-
return JSON.stringify(envelope);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function encodeStopConfirmationPrompt(input: {
|
|
97
|
-
confirmationId: string;
|
|
98
|
-
prompt?: string;
|
|
99
|
-
state?: StopConfirmationState;
|
|
100
|
-
}): string {
|
|
101
|
-
return encodeStopConfirmationEnvelope(
|
|
102
|
-
createStopConfirmationPromptEnvelope(input),
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function encodeStopConfirmationResult(input: {
|
|
107
|
-
confirmationId: string;
|
|
108
|
-
decision: StopConfirmationDecision;
|
|
109
|
-
}): string {
|
|
110
|
-
return encodeStopConfirmationEnvelope(
|
|
111
|
-
createStopConfirmationResultEnvelope(input),
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function parseStopConfirmationEnvelope(
|
|
116
|
-
content: string | null | undefined,
|
|
117
|
-
): StopConfirmationEnvelope | null {
|
|
118
|
-
if (typeof content !== "string" || content.trim() === "") {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let parsed: unknown;
|
|
123
|
-
try {
|
|
124
|
-
parsed = JSON.parse(content);
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!isRecord(parsed) || typeof parsed.kind !== "string") {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (parsed.kind === STOP_CONFIRMATION_KIND) {
|
|
134
|
-
if (typeof parsed.confirmationId !== "string" || !parsed.confirmationId.trim()) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
kind: STOP_CONFIRMATION_KIND,
|
|
140
|
-
confirmationId: parsed.confirmationId,
|
|
141
|
-
state: normalizeState(parsed.state),
|
|
142
|
-
prompt: normalizePrompt(parsed.prompt),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (parsed.kind === STOP_CONFIRMATION_RESULT_KIND) {
|
|
147
|
-
const decision = normalizeDecision(parsed.decision);
|
|
148
|
-
if (
|
|
149
|
-
typeof parsed.confirmationId !== "string" ||
|
|
150
|
-
!parsed.confirmationId.trim() ||
|
|
151
|
-
decision == null
|
|
152
|
-
) {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
kind: STOP_CONFIRMATION_RESULT_KIND,
|
|
158
|
-
confirmationId: parsed.confirmationId,
|
|
159
|
-
decision,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function foldStopConfirmationMessages<T extends StopConfirmationMessageLike>(
|
|
167
|
-
messages: T[],
|
|
168
|
-
): StopConfirmationRenderItem<T>[] {
|
|
169
|
-
const byConfirmationId = new Map<
|
|
170
|
-
string,
|
|
171
|
-
{
|
|
172
|
-
firstIndex: number;
|
|
173
|
-
latestIndex: number;
|
|
174
|
-
latestEnvelope: StopConfirmationEnvelope;
|
|
175
|
-
}
|
|
176
|
-
>();
|
|
177
|
-
|
|
178
|
-
messages.forEach((message, index) => {
|
|
179
|
-
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
180
|
-
if (!envelope) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const entry = byConfirmationId.get(envelope.confirmationId);
|
|
185
|
-
const sortValue = safeCreatedAtValue(message.createdAt, index);
|
|
186
|
-
if (!entry) {
|
|
187
|
-
byConfirmationId.set(envelope.confirmationId, {
|
|
188
|
-
firstIndex: index,
|
|
189
|
-
latestIndex: index,
|
|
190
|
-
latestEnvelope: envelope,
|
|
191
|
-
});
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const currentSortValue = safeCreatedAtValue(
|
|
196
|
-
messages[entry.latestIndex]?.createdAt,
|
|
197
|
-
entry.latestIndex,
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
if (sortValue >= currentSortValue) {
|
|
201
|
-
byConfirmationId.set(envelope.confirmationId, {
|
|
202
|
-
firstIndex: entry.firstIndex,
|
|
203
|
-
latestIndex: index,
|
|
204
|
-
latestEnvelope: envelope,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const renderItems: StopConfirmationRenderItem<T>[] = [];
|
|
210
|
-
|
|
211
|
-
messages.forEach((message, index) => {
|
|
212
|
-
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
213
|
-
if (!envelope) {
|
|
214
|
-
renderItems.push({ type: "message", message });
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const entry = byConfirmationId.get(envelope.confirmationId);
|
|
219
|
-
if (!entry || entry.firstIndex !== index) {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const promptEnvelope =
|
|
224
|
-
entry.latestEnvelope.kind === STOP_CONFIRMATION_KIND
|
|
225
|
-
? entry.latestEnvelope
|
|
226
|
-
: createStopConfirmationPromptEnvelope({
|
|
227
|
-
confirmationId: entry.latestEnvelope.confirmationId,
|
|
228
|
-
prompt: DEFAULT_STOP_CONVERSATION_PROMPT,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
renderItems.push({
|
|
232
|
-
type: "confirmation",
|
|
233
|
-
message,
|
|
234
|
-
envelope: promptEnvelope,
|
|
235
|
-
latestEnvelope: entry.latestEnvelope,
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
return renderItems;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function getPendingStopConfirmationId<T extends StopConfirmationMessageLike>(
|
|
243
|
-
messages: T[],
|
|
244
|
-
): string | null {
|
|
245
|
-
const latestByConfirmationId = new Map<
|
|
246
|
-
string,
|
|
247
|
-
{
|
|
248
|
-
sortValue: number;
|
|
249
|
-
envelope: StopConfirmationEnvelope;
|
|
250
|
-
}
|
|
251
|
-
>();
|
|
252
|
-
|
|
253
|
-
messages.forEach((message, index) => {
|
|
254
|
-
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
255
|
-
if (!envelope) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const sortValue = safeCreatedAtValue(message.createdAt, index);
|
|
260
|
-
const current = latestByConfirmationId.get(envelope.confirmationId);
|
|
261
|
-
if (!current || sortValue >= current.sortValue) {
|
|
262
|
-
latestByConfirmationId.set(envelope.confirmationId, {
|
|
263
|
-
sortValue,
|
|
264
|
-
envelope,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
for (const [confirmationId, entry] of latestByConfirmationId.entries()) {
|
|
270
|
-
if (
|
|
271
|
-
entry.envelope.kind === STOP_CONFIRMATION_KIND &&
|
|
272
|
-
entry.envelope.state === "pending"
|
|
273
|
-
) {
|
|
274
|
-
return confirmationId;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function safeCreatedAtValue(value: string | undefined, fallbackIndex: number): number {
|
|
282
|
-
if (typeof value !== "string") {
|
|
283
|
-
return fallbackIndex;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const timestamp = Date.parse(value);
|
|
287
|
-
return Number.isNaN(timestamp) ? fallbackIndex : timestamp;
|
|
288
|
-
}
|