@planningcenter/chat-react-native 3.12.2-qa-328.0 → 3.12.2-rc.1

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/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
  2. package/build/hooks/use_conversation_jolt_events.js +14 -2
  3. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  4. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  5. package/build/hooks/use_conversation_messages_jolt_events.js +6 -0
  6. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  7. package/build/hooks/use_conversations_jolt_events.d.ts.map +1 -1
  8. package/build/hooks/use_conversations_jolt_events.js +13 -1
  9. package/build/hooks/use_conversations_jolt_events.js.map +1 -1
  10. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  11. package/build/hooks/use_message_create_or_update.js +22 -1
  12. package/build/hooks/use_message_create_or_update.js.map +1 -1
  13. package/build/types/jolt_events/conversation_events.d.ts +1 -0
  14. package/build/types/jolt_events/conversation_events.d.ts.map +1 -1
  15. package/build/types/jolt_events/conversation_events.js.map +1 -1
  16. package/build/types/jolt_events/message_events.d.ts +1 -0
  17. package/build/types/jolt_events/message_events.d.ts.map +1 -1
  18. package/build/types/jolt_events/message_events.js.map +1 -1
  19. package/build/utils/performance_tracking.d.ts +26 -0
  20. package/build/utils/performance_tracking.d.ts.map +1 -0
  21. package/build/utils/performance_tracking.js +100 -0
  22. package/build/utils/performance_tracking.js.map +1 -0
  23. package/package.json +2 -2
  24. package/src/hooks/use_conversation_jolt_events.ts +19 -3
  25. package/src/hooks/use_conversation_messages_jolt_events.ts +6 -0
  26. package/src/hooks/use_conversations_jolt_events.ts +14 -2
  27. package/src/hooks/use_message_create_or_update.ts +23 -1
  28. package/src/types/jolt_events/conversation_events.ts +1 -0
  29. package/src/types/jolt_events/message_events.ts +1 -0
  30. package/src/utils/performance_tracking.ts +128 -0
@@ -1 +1 @@
1
- {"version":3,"file":"use_conversation_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"AAQA,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,yBAAyB,CAAC,EAAE,cAAc,EAAE,EAAE,KAAK,QAqDlE"}
1
+ {"version":3,"file":"use_conversation_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"AAWA,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,yBAAyB,CAAC,EAAE,cAAc,EAAE,EAAE,KAAK,QAkElE"}
@@ -3,13 +3,25 @@ import { getConversationRequestArgs } from './use_conversation';
3
3
  import { useQueryClient } from '@tanstack/react-query';
4
4
  import { getRequestQueryKey } from './use_suspense_api';
5
5
  import { useCallback, useMemo } from 'react';
6
+ import { completeMessageCreationConversationTracking } from '../utils/performance_tracking';
7
+ import { useCurrentPerson } from './use_current_person';
8
+ import { useApiClient } from './use_api_client';
6
9
  export function useConversationJoltEvents({ conversationId }) {
7
10
  const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`);
8
11
  const queryClient = useQueryClient();
12
+ const currentPerson = useCurrentPerson();
13
+ const apiClient = useApiClient();
9
14
  const queryKey = useMemo(() => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })), [conversationId]);
10
- const handleUpdatedConversation = useCallback(() => {
15
+ const handleUpdatedConversation = useCallback((e) => {
16
+ const { last_message_idempotent_key, last_message_author_id } = e.data.data;
17
+ if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {
18
+ completeMessageCreationConversationTracking({
19
+ apiClient,
20
+ idempotentKey: last_message_idempotent_key,
21
+ });
22
+ }
11
23
  queryClient.invalidateQueries({ queryKey });
12
- }, [queryClient, queryKey]);
24
+ }, [queryClient, queryKey, currentPerson.id, apiClient]);
13
25
  const handleConversationRead = useCallback((e) => {
14
26
  const { latest_read_message_sort_key } = e.data.data;
15
27
  queryClient.setQueryData(queryKey, prev => {
@@ -1 +1 @@
1
- {"version":3,"file":"use_conversation_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAM5C,MAAM,UAAU,yBAAyB,CAAC,EAAE,cAAc,EAAS;IACjE,MAAM,WAAW,GAAG,cAAc,CAAC,sBAAsB,cAAc,EAAE,CAAC,CAAA;IAC1E,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IACpC,MAAM,QAAQ,GAAG,OAAO,CACtB,GAAG,EAAE,CAAC,kBAAkB,CAAC,0BAA0B,CAAC,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC,CAAC,EACzF,CAAC,cAAc,CAAC,CACjB,CAAA;IAED,MAAM,yBAAyB,GAAG,WAAW,CAAC,GAAG,EAAE;QACjD,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC7C,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAA;IAE3B,MAAM,sBAAsB,GAAG,WAAW,CACxC,CAAC,CAAwB,EAAE,EAAE;QAC3B,MAAM,EAAE,4BAA4B,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAA;QACpD,WAAW,CAAC,YAAY,CAAoC,QAAQ,EAAE,IAAI,CAAC,EAAE;YAC3E,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,4BAA4B;gBAAE,OAAO,IAAI,CAAA;YAC7D,IACE,IAAI,CAAC,IAAI,CAAC,wBAAwB;gBAClC,IAAI,CAAC,IAAI,CAAC,wBAAwB,IAAI,4BAA4B,EAClE,CAAC;gBACD,OAAO,IAAI,CAAA;YACb,CAAC;YAED,OAAO;gBACL,GAAG,IAAI;gBACP,IAAI,EAAE;oBACJ,GAAG,IAAI,CAAC,IAAI;oBACZ,wBAAwB,EAAE,4BAA4B;iBACvD;aACF,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,EACD,CAAC,WAAW,EAAE,QAAQ,CAAC,CACxB,CAAA;IAED,MAAM,2BAA2B,GAAG,WAAW,CAAC,GAAG,EAAE;QACnD,WAAW,CAAC,YAAY,CAAoC,QAAQ,EAAE,IAAI,CAAC,EAAE;YAC3E,IAAI,CAAC,IAAI,EAAE,IAAI;gBAAE,OAAO,IAAI,CAAA;YAE5B,OAAO;gBACL,GAAG,IAAI;gBACP,IAAI,EAAE;oBACJ,GAAG,IAAI,CAAC,IAAI;oBACZ,OAAO,EAAE,IAAI;iBACd;aACF,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAA;IAE3B,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,yBAAyB,CAAC,CAAA;IAC5E,YAAY,CAAC,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,CAAC,CAAA;IACtE,YAAY,CAAC,WAAW,EAAE,wBAAwB,EAAE,2BAA2B,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { JoltConversationEvent } from '../types/jolt_events'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport { getConversationRequestArgs } from './use_conversation'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { ApiResource, ConversationResource } from '../types'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { useCallback, useMemo } from 'react'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationJoltEvents({ conversationId }: Props) {\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const queryClient = useQueryClient()\n const queryKey = useMemo(\n () => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })),\n [conversationId]\n )\n\n const handleUpdatedConversation = useCallback(() => {\n queryClient.invalidateQueries({ queryKey })\n }, [queryClient, queryKey])\n\n const handleConversationRead = useCallback(\n (e: JoltConversationEvent) => {\n const { latest_read_message_sort_key } = e.data.data\n queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {\n if (!prev?.data || !latest_read_message_sort_key) return prev\n if (\n prev.data.latestReadMessageSortKey &&\n prev.data.latestReadMessageSortKey >= latest_read_message_sort_key\n ) {\n return prev\n }\n\n return {\n ...prev,\n data: {\n ...prev.data,\n latestReadMessageSortKey: latest_read_message_sort_key,\n },\n }\n })\n },\n [queryClient, queryKey]\n )\n\n const handleConversationDestroyed = useCallback(() => {\n queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {\n if (!prev?.data) return prev\n\n return {\n ...prev,\n data: {\n ...prev.data,\n deleted: true,\n },\n }\n })\n }, [queryClient, queryKey])\n\n useJoltEvent(joltChannel, 'conversation.updated', handleUpdatedConversation)\n useJoltEvent(joltChannel, 'conversation.read', handleConversationRead)\n useJoltEvent(joltChannel, 'conversation.destroyed', handleConversationDestroyed)\n}\n"]}
1
+ {"version":3,"file":"use_conversation_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"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","sourcesContent":["import { JoltConversationEvent } from '../types/jolt_events'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport { getConversationRequestArgs } from './use_conversation'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { ApiResource, ConversationResource } from '../types'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { useCallback, useMemo } from 'react'\nimport { completeMessageCreationConversationTracking } from '../utils/performance_tracking'\nimport { useCurrentPerson } from './use_current_person'\nimport { useApiClient } from './use_api_client'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationJoltEvents({ conversationId }: Props) {\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const queryClient = useQueryClient()\n const currentPerson = useCurrentPerson()\n const apiClient = useApiClient()\n const queryKey = useMemo(\n () => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })),\n [conversationId]\n )\n\n const handleUpdatedConversation = useCallback(\n (e: JoltConversationEvent) => {\n const { last_message_idempotent_key, last_message_author_id } = e.data.data\n\n if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {\n completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey: last_message_idempotent_key,\n })\n }\n queryClient.invalidateQueries({ queryKey })\n },\n [queryClient, queryKey, currentPerson.id, apiClient]\n )\n\n const handleConversationRead = useCallback(\n (e: JoltConversationEvent) => {\n const { latest_read_message_sort_key } = e.data.data\n queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {\n if (!prev?.data || !latest_read_message_sort_key) return prev\n if (\n prev.data.latestReadMessageSortKey &&\n prev.data.latestReadMessageSortKey >= latest_read_message_sort_key\n ) {\n return prev\n }\n\n return {\n ...prev,\n data: {\n ...prev.data,\n latestReadMessageSortKey: latest_read_message_sort_key,\n },\n }\n })\n },\n [queryClient, queryKey]\n )\n\n const handleConversationDestroyed = useCallback(() => {\n queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {\n if (!prev?.data) return prev\n\n return {\n ...prev,\n data: {\n ...prev.data,\n deleted: true,\n },\n }\n })\n }, [queryClient, queryKey])\n\n useJoltEvent(joltChannel, 'conversation.updated', handleUpdatedConversation)\n useJoltEvent(joltChannel, 'conversation.read', handleConversationRead)\n useJoltEvent(joltChannel, 'conversation.destroyed', handleConversationDestroyed)\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"use_conversation_messages_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"AAkBA,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,iCAAiC,CAAC,EAAE,cAAc,EAAE,EAAE,KAAK,QA2H1E"}
1
+ {"version":3,"file":"use_conversation_messages_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"AAoBA,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,iCAAiC,CAAC,EAAE,cAAc,EAAE,EAAE,KAAK,QA+H1E"}
@@ -8,12 +8,15 @@ import { transformReactionEventDataToReactionCountResource } from '../utils/jolt
8
8
  import { getMessagesRequestArgs } from '../utils/request/messages';
9
9
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache';
10
10
  import { isTemporaryMessageId } from './use_message_create_or_update';
11
+ import { completeMessageCreationTracking } from '../utils/performance_tracking';
12
+ import { useApiClient } from './use_api_client';
11
13
  export function useConversationMessagesJoltEvents({ conversationId }) {
12
14
  const queryClient = useQueryClient();
13
15
  const currentPerson = useCurrentPerson();
14
16
  const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`);
15
17
  const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId });
16
18
  const messagesQueryKey = getRequestQueryKey(messagesRequestArgs);
19
+ const apiClient = useApiClient();
17
20
  const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } = useTypingStatusCache(conversationId);
18
21
  const handleMessageUpdateOrCreate = async (e) => {
19
22
  const { data } = e.data;
@@ -23,6 +26,9 @@ export function useConversationMessagesJoltEvents({ conversationId }) {
23
26
  });
24
27
  if (e.event === 'message.created' && data.author_id) {
25
28
  removeAllTypingEventsByAuthorId(data.author_id);
29
+ if (data.idempotent_key && data.author_id === currentPerson.id) {
30
+ completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key });
31
+ }
26
32
  }
27
33
  queryClient.setQueryData(messagesQueryKey, prev => {
28
34
  if (e.event === 'message.created') {
@@ -1 +1 @@
1
- {"version":3,"file":"use_conversation_messages_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCollection, MessageResource } from '../types'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'\nimport { InfiniteData, useQueryClient } from '@tanstack/react-query'\nimport { useCurrentPerson } from './use_current_person'\nimport { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'\nimport { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'\nimport { getMessagesRequestArgs } from '../utils/request/messages'\nimport { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'\nimport { isTemporaryMessageId } from './use_message_create_or_update'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationMessagesJoltEvents({ conversationId }: Props) {\n const queryClient = useQueryClient()\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })\n const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)\n\n const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =\n useTypingStatusCache(conversationId)\n\n const handleMessageUpdateOrCreate = async (e: MessageCreatedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n if (e.event === 'message.created' && data.author_id) {\n removeAllTypingEventsByAuthorId(data.author_id)\n }\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {\n if (e.event === 'message.created') {\n // Before adding the new message, remove any pending temporary messages\n // with matching text to prevent duplicates from race conditions\n let dataAfterTempRemoval = prev\n if (prev && message.text && message.mine) {\n dataAfterTempRemoval = deleteRecordInPagesData({\n data: prev,\n record: message,\n matchFn: (existingMessage, _record) => {\n return (\n isTemporaryMessageId(existingMessage.id) &&\n existingMessage.text === message.text &&\n existingMessage.mine\n )\n },\n })\n }\n\n return updateOrCreateRecordInPagesData({\n data: dataAfterTempRemoval,\n record: message,\n processRecord: (record, current) => {\n return { ...current, ...record }\n },\n })\n } else {\n return updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, current) => {\n return { ...current, ...record }\n },\n })\n }\n })\n }\n\n const handleMessageDeleted = async (e: MessageDeletedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>\n deleteRecordInPagesData({ data: prev, record: message })\n )\n }\n\n const handleReactionJoltEvent = async (e: JoltReactionEvent) => {\n const { data } = e.data\n const message = { id: data.message_sort_key } as MessageResource\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>\n updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, oldMessage) => {\n const reactionCounts = oldMessage.reactionCounts || []\n let foundMatch = false\n let newReactionCounts = reactionCounts.map(reactionCount => {\n if (reactionCount.value === data.value) {\n foundMatch = true\n return transformReactionEventDataToReactionCountResource({\n data,\n oldData: reactionCount,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n }\n return reactionCount\n })\n\n if (!foundMatch) {\n const newReactionCount = transformReactionEventDataToReactionCountResource({\n data,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n\n if (newReactionCount?.count) {\n newReactionCounts = [...newReactionCounts, newReactionCount]\n }\n }\n\n return { ...oldMessage, reactionCounts: newReactionCounts }\n },\n })\n )\n }\n\n const handleTypingEvent = async (e: JoltTypingEvent) => {\n const { data } = e.data\n addTypingEvent(data)\n setTimeout(() => removeTypingEventById(data.id), TYPING_TIMEOUT_INTERVAL)\n }\n\n useJoltEvent(joltChannel, 'message.created', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.updated', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.destroyed', handleMessageDeleted)\n useJoltEvent(joltChannel, 'reaction.*', handleReactionJoltEvent)\n useJoltEvent(joltChannel, 'typing.broadcast', handleTypingEvent)\n}\n\ntype QueryData = InfiniteData<ApiCollection<MessageResource>>\n"]}
1
+ {"version":3,"file":"use_conversation_messages_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCollection, MessageResource } from '../types'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'\nimport { InfiniteData, useQueryClient } from '@tanstack/react-query'\nimport { useCurrentPerson } from './use_current_person'\nimport { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'\nimport { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'\nimport { getMessagesRequestArgs } from '../utils/request/messages'\nimport { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'\nimport { isTemporaryMessageId } from './use_message_create_or_update'\nimport { completeMessageCreationTracking } from '../utils/performance_tracking'\nimport { useApiClient } from './use_api_client'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationMessagesJoltEvents({ conversationId }: Props) {\n const queryClient = useQueryClient()\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })\n const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)\n const apiClient = useApiClient()\n\n const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =\n useTypingStatusCache(conversationId)\n\n const handleMessageUpdateOrCreate = async (e: MessageCreatedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n if (e.event === 'message.created' && data.author_id) {\n removeAllTypingEventsByAuthorId(data.author_id)\n if (data.idempotent_key && data.author_id === currentPerson.id) {\n completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key })\n }\n }\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {\n if (e.event === 'message.created') {\n // Before adding the new message, remove any pending temporary messages\n // with matching text to prevent duplicates from race conditions\n let dataAfterTempRemoval = prev\n if (prev && message.text && message.mine) {\n dataAfterTempRemoval = deleteRecordInPagesData({\n data: prev,\n record: message,\n matchFn: (existingMessage, _record) => {\n return (\n isTemporaryMessageId(existingMessage.id) &&\n existingMessage.text === message.text &&\n existingMessage.mine\n )\n },\n })\n }\n\n return updateOrCreateRecordInPagesData({\n data: dataAfterTempRemoval,\n record: message,\n processRecord: (record, current) => {\n return { ...current, ...record }\n },\n })\n } else {\n return updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, current) => {\n return { ...current, ...record }\n },\n })\n }\n })\n }\n\n const handleMessageDeleted = async (e: MessageDeletedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>\n deleteRecordInPagesData({ data: prev, record: message })\n )\n }\n\n const handleReactionJoltEvent = async (e: JoltReactionEvent) => {\n const { data } = e.data\n const message = { id: data.message_sort_key } as MessageResource\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>\n updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, oldMessage) => {\n const reactionCounts = oldMessage.reactionCounts || []\n let foundMatch = false\n let newReactionCounts = reactionCounts.map(reactionCount => {\n if (reactionCount.value === data.value) {\n foundMatch = true\n return transformReactionEventDataToReactionCountResource({\n data,\n oldData: reactionCount,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n }\n return reactionCount\n })\n\n if (!foundMatch) {\n const newReactionCount = transformReactionEventDataToReactionCountResource({\n data,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n\n if (newReactionCount?.count) {\n newReactionCounts = [...newReactionCounts, newReactionCount]\n }\n }\n\n return { ...oldMessage, reactionCounts: newReactionCounts }\n },\n })\n )\n }\n\n const handleTypingEvent = async (e: JoltTypingEvent) => {\n const { data } = e.data\n addTypingEvent(data)\n setTimeout(() => removeTypingEventById(data.id), TYPING_TIMEOUT_INTERVAL)\n }\n\n useJoltEvent(joltChannel, 'message.created', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.updated', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.destroyed', handleMessageDeleted)\n useJoltEvent(joltChannel, 'reaction.*', handleReactionJoltEvent)\n useJoltEvent(joltChannel, 'typing.broadcast', handleTypingEvent)\n}\n\ntype QueryData = InfiniteData<ApiCollection<MessageResource>>\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"use_conversations_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAA;AAKvE,wBAAgB,0BAA0B,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,QAkBjF"}
1
+ {"version":3,"file":"use_conversations_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAA;AAOvE,wBAAgB,0BAA0B,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,QA4BjF"}
@@ -1,12 +1,24 @@
1
1
  import { useConversationsCache } from './use_conversations_cache';
2
2
  import { useCurrentPerson, useCurrentPersonCache } from './use_current_person';
3
3
  import { useJoltChannel, useJoltEvent } from './use_jolt';
4
+ import { completeMessageCreationConversationTracking } from '../utils/performance_tracking';
5
+ import { useApiClient } from './use_api_client';
4
6
  export function useConversationsJoltEvents(args) {
5
7
  const currentPerson = useCurrentPerson();
6
8
  const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`);
7
9
  const cache = useConversationsCache(args);
8
10
  const currentPersonCache = useCurrentPersonCache();
9
- useJoltEvent(joltChannel, 'conversation.updated', (e) => cache.fetchUpdate({ id: e.data.data.id }));
11
+ const apiClient = useApiClient();
12
+ useJoltEvent(joltChannel, 'conversation.updated', (e) => {
13
+ if (e.data.data.last_message_idempotent_key &&
14
+ e.data.data.last_message_author_id === currentPerson.id) {
15
+ completeMessageCreationConversationTracking({
16
+ apiClient,
17
+ idempotentKey: e.data.data.last_message_idempotent_key,
18
+ });
19
+ }
20
+ cache.fetchUpdate({ id: e.data.data.id });
21
+ });
10
22
  useJoltEvent(joltChannel, 'conversation.created', (e) => cache.fetchCreate({ id: e.data.data.id }));
11
23
  useJoltEvent(joltChannel, 'conversation.destroyed', (e) => cache.destroy({ id: e.data.data.id }));
12
24
  useJoltEvent(joltChannel, 'STREAM_USER_UPDATED', currentPersonCache.invalidate); // Would be nice to have a non-stream chat event
@@ -1 +1 @@
1
- {"version":3,"file":"use_conversations_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEzD,MAAM,UAAU,0BAA0B,CAAC,IAAuC;IAChF,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAA;IACxC,MAAM,WAAW,GAAG,cAAc,CAAC,eAAe,aAAa,CAAC,EAAE,EAAE,CAAC,CAAA;IACrE,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACzC,MAAM,kBAAkB,GAAG,qBAAqB,EAAE,CAAA;IAElD,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC7E,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAC1C,CAAA;IACD,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC7E,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAC1C,CAAA;IACD,YAAY,CAAC,WAAW,EAAE,wBAAwB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC/E,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CACtC,CAAA;IAED,YAAY,CAAC,WAAW,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA,CAAC,gDAAgD;IAChI,YAAY,CAAC,WAAW,EAAE,mBAAmB,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;AAC/E,CAAC","sourcesContent":["import { JoltConversationEvent } from '../types/jolt_events'\nimport { ConversationRequestArgs } from '../utils/request/conversation'\nimport { useConversationsCache } from './use_conversations_cache'\nimport { useCurrentPerson, useCurrentPersonCache } from './use_current_person'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\n\nexport function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)\n const cache = useConversationsCache(args)\n const currentPersonCache = useCurrentPersonCache()\n\n useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) =>\n cache.fetchUpdate({ id: e.data.data.id })\n )\n useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>\n cache.fetchCreate({ id: e.data.data.id })\n )\n useJoltEvent(joltChannel, 'conversation.destroyed', (e: JoltConversationEvent) =>\n cache.destroy({ id: e.data.data.id })\n )\n\n useJoltEvent(joltChannel, 'STREAM_USER_UPDATED', currentPersonCache.invalidate) // Would be nice to have a non-stream chat event\n useJoltEvent(joltChannel, 'conversation.read', currentPersonCache.invalidate)\n}\n"]}
1
+ {"version":3,"file":"use_conversations_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,2CAA2C,EAAE,MAAM,+BAA+B,CAAA;AAC3F,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,MAAM,UAAU,0BAA0B,CAAC,IAAuC;IAChF,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAA;IACxC,MAAM,WAAW,GAAG,cAAc,CAAC,eAAe,aAAa,CAAC,EAAE,EAAE,CAAC,CAAA;IACrE,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACzC,MAAM,kBAAkB,GAAG,qBAAqB,EAAE,CAAA;IAClD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAEhC,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAwB,EAAE,EAAE;QAC7E,IACE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B;YACvC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,sBAAsB,KAAK,aAAa,CAAC,EAAE,EACvD,CAAC;YACD,2CAA2C,CAAC;gBAC1C,SAAS;gBACT,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B;aACvD,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IACF,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC7E,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAC1C,CAAA;IACD,YAAY,CAAC,WAAW,EAAE,wBAAwB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC/E,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CACtC,CAAA;IAED,YAAY,CAAC,WAAW,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA,CAAC,gDAAgD;IAChI,YAAY,CAAC,WAAW,EAAE,mBAAmB,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;AAC/E,CAAC","sourcesContent":["import { JoltConversationEvent } from '../types/jolt_events'\nimport { ConversationRequestArgs } from '../utils/request/conversation'\nimport { useConversationsCache } from './use_conversations_cache'\nimport { useCurrentPerson, useCurrentPersonCache } from './use_current_person'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport { completeMessageCreationConversationTracking } from '../utils/performance_tracking'\nimport { useApiClient } from './use_api_client'\n\nexport function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)\n const cache = useConversationsCache(args)\n const currentPersonCache = useCurrentPersonCache()\n const apiClient = useApiClient()\n\n useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) => {\n if (\n e.data.data.last_message_idempotent_key &&\n e.data.data.last_message_author_id === currentPerson.id\n ) {\n completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey: e.data.data.last_message_idempotent_key,\n })\n }\n cache.fetchUpdate({ id: e.data.data.id })\n })\n useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>\n cache.fetchCreate({ id: e.data.data.id })\n )\n useJoltEvent(joltChannel, 'conversation.destroyed', (e: JoltConversationEvent) =>\n cache.destroy({ id: e.data.data.id })\n )\n\n useJoltEvent(joltChannel, 'STREAM_USER_UPDATED', currentPersonCache.invalidate) // Would be nice to have a non-stream chat event\n useJoltEvent(joltChannel, 'conversation.read', currentPersonCache.invalidate)\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"use_message_create_or_update.d.ts","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAOtE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gEAAgE,CAAA;AAKxH,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,eAAe,CAAA;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,KAAK;UA2C/D,MAAM;kBACE,uCAAuC,EAAE;;;GAqE5D;AAED,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEvE;AACD,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAE/D"}
1
+ {"version":3,"file":"use_message_create_or_update.d.ts","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAOtE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gEAAgE,CAAA;AAMxH,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,eAAe,CAAA;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,KAAK;UAiD/D,MAAM;kBACE,uCAAuC,EAAE;;;GAqE5D;AAED,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEvE;AACD,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAE/D"}
@@ -6,6 +6,7 @@ import { deleteRecordInPagesData, updateOrCreateRecordInPagesData, updateRecordI
6
6
  import { useCurrentPerson } from './use_current_person';
7
7
  import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message';
8
8
  import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message';
9
+ import { startMessageCreationTracking } from '../utils/performance_tracking';
9
10
  export function useMessageCreateOrUpdate({ conversationId, message }) {
10
11
  const messageId = message?.id || null;
11
12
  const isEditing = !isNewMessage(message);
@@ -15,11 +16,17 @@ export function useMessageCreateOrUpdate({ conversationId, message }) {
15
16
  mutationFn: ({ text, attachments, }) => {
16
17
  const requestParams = getMessagesRequestArgs({ conversation_id: conversationId });
17
18
  const fieldsWithValueJoined = Object.fromEntries(Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')]));
19
+ let attributes = { text, ...(attachments ? { attachments } : {}) };
20
+ if (!isEditing) {
21
+ const idempotentKey = insecureUUID();
22
+ attributes.idempotent_key = idempotentKey;
23
+ startMessageCreationTracking(idempotentKey);
24
+ }
18
25
  const data = {
19
26
  ...requestParams.data,
20
27
  data: {
21
28
  type: 'Message',
22
- attributes: { text, ...(attachments ? { attachments } : {}) },
29
+ attributes,
23
30
  },
24
31
  fields: fieldsWithValueJoined,
25
32
  };
@@ -96,4 +103,18 @@ export function isTemporaryMessageId(messageId) {
96
103
  export function isNewMessage(message) {
97
104
  return !message?.id || isTemporaryMessageId(message.id);
98
105
  }
106
+ /**
107
+ * Generate a random UUID (v4) for idempotent keys.
108
+ * Uses Math.random, which is not cryptographically secure.
109
+ * An actual crypto library requires native dependencies.
110
+ * This is OK for now since idempotent keys are not security-sensitive
111
+ * or need to be guaranteed unique.
112
+ * They are short lived and we use it in combination with the message's creator_id so
113
+ * their impact is scoped only the current user.
114
+ */
115
+ function insecureUUID() {
116
+ return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'
117
+ .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))
118
+ .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16));
119
+ }
99
120
  //# sourceMappingURL=use_message_create_or_update.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use_message_create_or_update.js","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"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","sourcesContent":["import { InfiniteData, useMutation } from '@tanstack/react-query'\nimport { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'\nimport { useApiClient } from './use_api_client'\nimport { ApiCollection, ApiResource, MessageResource } from '../types'\nimport { chatQueryClient } from '../contexts/api_provider'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'\nimport { useCurrentPerson } from './use_current_person'\nimport { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'\nimport { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'\n\ninterface Props {\n conversationId: number\n message?: MessageResource\n}\n\nexport function useMessageCreateOrUpdate({ conversationId, message }: Props) {\n const messageId = message?.id || null\n const isEditing = !isNewMessage(message)\n const apiClient = useApiClient()\n const currentPerson = useCurrentPerson()\n\n const mutation = useMutation({\n mutationFn: ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })\n const fieldsWithValueJoined = Object.fromEntries(\n Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])\n )\n const data = {\n ...requestParams.data,\n data: {\n type: 'Message',\n attributes: { text, ...(attachments ? { attachments } : {}) },\n },\n fields: fieldsWithValueJoined,\n }\n\n if (isEditing) {\n return apiClient.chat.patch<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages/${messageId}`,\n data,\n })\n } else {\n return apiClient.chat.post<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages`,\n data,\n })\n }\n },\n onMutate: async ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n if (message && isEditing) {\n const optimisticMessage = optimisticallyUpdateMessage({\n conversationId,\n message,\n text,\n })\n\n return { message: optimisticMessage }\n }\n\n const optimisticMessage = optimisticallyCreateMessage({\n conversationId,\n message,\n text,\n attachments,\n currentPerson,\n })\n\n return { message: optimisticMessage }\n },\n onError: (error, variables, context) => {\n const { message: optimisticMessage } = context || {}\n // Add error to the optimistic message from the cache on error\n if (optimisticMessage) {\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n chatQueryClient.setQueryData(\n queryKey,\n (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>\n updateRecordInPagesData({\n data,\n record: optimisticMessage,\n processRecord: (_next, prev) => ({\n ...prev,\n error: error.message || 'Failed to send message',\n pending: false, // Mark as no longer pending\n }),\n })\n )\n }\n },\n onSuccess: (result: ApiResource<MessageResource>, variables, context) => {\n const { message: optimisticMessage } = context || {}\n const updatedMessage = result.data\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n\n // First remove the optimistic message if it exists\n if (optimisticMessage) {\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n deleteRecordInPagesData({\n data,\n record: optimisticMessage,\n })\n )\n }\n\n // Then add the real message\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: updatedMessage,\n })\n )\n },\n })\n\n return mutation\n}\n\nexport function isTemporaryMessageId(messageId?: string | null): boolean {\n return !!messageId && messageId.endsWith('-temp')\n}\nexport function isNewMessage(message?: MessageResource): boolean {\n return !message?.id || isTemporaryMessageId(message.id)\n}\n"]}
1
+ {"version":3,"file":"use_message_create_or_update.js","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"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","sourcesContent":["import { InfiniteData, useMutation } from '@tanstack/react-query'\nimport { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'\nimport { useApiClient } from './use_api_client'\nimport { ApiCollection, ApiResource, MessageResource } from '../types'\nimport { chatQueryClient } from '../contexts/api_provider'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'\nimport { useCurrentPerson } from './use_current_person'\nimport { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'\nimport { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'\nimport { startMessageCreationTracking } from '../utils/performance_tracking'\n\ninterface Props {\n conversationId: number\n message?: MessageResource\n}\n\nexport function useMessageCreateOrUpdate({ conversationId, message }: Props) {\n const messageId = message?.id || null\n const isEditing = !isNewMessage(message)\n const apiClient = useApiClient()\n const currentPerson = useCurrentPerson()\n\n const mutation = useMutation({\n mutationFn: ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })\n const fieldsWithValueJoined = Object.fromEntries(\n Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])\n )\n let attributes: any = { text, ...(attachments ? { attachments } : {}) }\n if (!isEditing) {\n const idempotentKey = insecureUUID()\n attributes.idempotent_key = idempotentKey\n startMessageCreationTracking(idempotentKey)\n }\n const data = {\n ...requestParams.data,\n data: {\n type: 'Message',\n attributes,\n },\n fields: fieldsWithValueJoined,\n }\n\n if (isEditing) {\n return apiClient.chat.patch<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages/${messageId}`,\n data,\n })\n } else {\n return apiClient.chat.post<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages`,\n data,\n })\n }\n },\n onMutate: async ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n if (message && isEditing) {\n const optimisticMessage = optimisticallyUpdateMessage({\n conversationId,\n message,\n text,\n })\n\n return { message: optimisticMessage }\n }\n\n const optimisticMessage = optimisticallyCreateMessage({\n conversationId,\n message,\n text,\n attachments,\n currentPerson,\n })\n\n return { message: optimisticMessage }\n },\n onError: (error, variables, context) => {\n const { message: optimisticMessage } = context || {}\n // Add error to the optimistic message from the cache on error\n if (optimisticMessage) {\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n chatQueryClient.setQueryData(\n queryKey,\n (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>\n updateRecordInPagesData({\n data,\n record: optimisticMessage,\n processRecord: (_next, prev) => ({\n ...prev,\n error: error.message || 'Failed to send message',\n pending: false, // Mark as no longer pending\n }),\n })\n )\n }\n },\n onSuccess: (result: ApiResource<MessageResource>, variables, context) => {\n const { message: optimisticMessage } = context || {}\n const updatedMessage = result.data\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n\n // First remove the optimistic message if it exists\n if (optimisticMessage) {\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n deleteRecordInPagesData({\n data,\n record: optimisticMessage,\n })\n )\n }\n\n // Then add the real message\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: updatedMessage,\n })\n )\n },\n })\n\n return mutation\n}\n\nexport function isTemporaryMessageId(messageId?: string | null): boolean {\n return !!messageId && messageId.endsWith('-temp')\n}\nexport function isNewMessage(message?: MessageResource): boolean {\n return !message?.id || isTemporaryMessageId(message.id)\n}\n\n/**\n * Generate a random UUID (v4) for idempotent keys.\n * Uses Math.random, which is not cryptographically secure.\n * An actual crypto library requires native dependencies.\n * This is OK for now since idempotent keys are not security-sensitive\n * or need to be guaranteed unique.\n * They are short lived and we use it in combination with the message's creator_id so\n * their impact is scoped only the current user.\n */\nfunction insecureUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'\n .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))\n .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))\n}\n"]}
@@ -9,6 +9,7 @@ interface BaseConversationEventData extends Record<string, unknown> {
9
9
  last_message_author_name?: string;
10
10
  last_message_created_at?: DateString;
11
11
  last_message_sort_key?: string;
12
+ last_message_idempotent_key?: string | null;
12
13
  last_message_text_preview?: string;
13
14
  latest_read_message_sort_key?: string;
14
15
  organization_id: number;
@@ -1 +1 @@
1
- {"version":3,"file":"conversation_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,KAAK,UAAU,GAAG,MAAM,CAAA;AACxB,UAAU,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,UAAU,CAAA;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,uBAAuB,CAAC,EAAE,UAAU,CAAA;QACpC,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,UAAU,CAAA;KACvB,CAAA;CACF;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,wBAAwB,CAAA;IAC/B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,yBAAyB,CAAA;CAChC"}
1
+ {"version":3,"file":"conversation_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,KAAK,UAAU,GAAG,MAAM,CAAA;AACxB,UAAU,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,UAAU,CAAA;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,uBAAuB,CAAC,EAAE,UAAU,CAAA;QACpC,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3C,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,UAAU,CAAA;KACvB,CAAA;CACF;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,wBAAwB,CAAA;IAC/B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,yBAAyB,CAAA;CAChC"}
@@ -1 +1 @@
1
- {"version":3,"file":"conversation_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ntype DateString = string\ninterface BaseConversationEventData extends Record<string, unknown> {\n data: {\n id: number\n archived_at: DateString\n created_at: DateString\n last_message_author_id?: number\n last_message_author_name?: string\n last_message_created_at?: DateString\n last_message_sort_key?: string\n last_message_text_preview?: string\n latest_read_message_sort_key?: string\n organization_id: number\n replies_disabled: boolean\n subtitle?: string\n title: string\n updated_at: DateString\n }\n}\n\nexport interface ConversationCreatedEvent extends CustomMessage {\n event: 'conversation.created'\n data: BaseConversationEventData\n}\n\nexport interface ConversationUpdatedEvent extends CustomMessage {\n event: 'conversation.updated'\n data: BaseConversationEventData\n}\n\nexport interface ConversationDeletedEvent extends CustomMessage {\n event: 'conversation.destroyed'\n data: BaseConversationEventData\n}\n\nexport interface ConversationReadEvent extends CustomMessage {\n event: 'conversation.read'\n data: BaseConversationEventData\n}\n"]}
1
+ {"version":3,"file":"conversation_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ntype DateString = string\ninterface BaseConversationEventData extends Record<string, unknown> {\n data: {\n id: number\n archived_at: DateString\n created_at: DateString\n last_message_author_id?: number\n last_message_author_name?: string\n last_message_created_at?: DateString\n last_message_sort_key?: string\n last_message_idempotent_key?: string | null\n last_message_text_preview?: string\n latest_read_message_sort_key?: string\n organization_id: number\n replies_disabled: boolean\n subtitle?: string\n title: string\n updated_at: DateString\n }\n}\n\nexport interface ConversationCreatedEvent extends CustomMessage {\n event: 'conversation.created'\n data: BaseConversationEventData\n}\n\nexport interface ConversationUpdatedEvent extends CustomMessage {\n event: 'conversation.updated'\n data: BaseConversationEventData\n}\n\nexport interface ConversationDeletedEvent extends CustomMessage {\n event: 'conversation.destroyed'\n data: BaseConversationEventData\n}\n\nexport interface ConversationReadEvent extends CustomMessage {\n event: 'conversation.read'\n data: BaseConversationEventData\n}\n"]}
@@ -14,6 +14,7 @@ interface BaseMessageEventData extends Record<string, unknown> {
14
14
  text_edited_at: string | null;
15
15
  html: string;
16
16
  attachments: DenormalizedAttachmentResource[];
17
+ idempotent_key?: string | null;
17
18
  };
18
19
  }
19
20
  export interface MessageCreatedEvent extends CustomMessage {
@@ -1 +1 @@
1
- {"version":3,"file":"message_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAA;AAE9F,UAAU,oBAAqB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAA;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,8BAA8B,EAAE,CAAA;KAC9C,CAAA;CACF;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,oBAAoB,CAAA;CAC3B"}
1
+ {"version":3,"file":"message_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAA;AAE9F,UAAU,oBAAqB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAA;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,8BAA8B,EAAE,CAAA;QAC7C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC/B,CAAA;CACF;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,oBAAoB,CAAA;CAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"message_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'\n\ninterface BaseMessageEventData extends Record<string, unknown> {\n data: {\n author_avatar: string\n author_id: number\n author_name: string\n conversation_id: number\n created_at: string\n deleted_at: string | null\n organization_id: number | null\n sort_key: string\n text: string\n text_edited_at: string | null\n html: string\n attachments: DenormalizedAttachmentResource[]\n }\n}\n\nexport interface MessageCreatedEvent extends CustomMessage {\n event: 'message.created'\n data: BaseMessageEventData\n}\n\nexport interface MessageUpdatedEvent extends CustomMessage {\n event: 'message.updated'\n data: BaseMessageEventData\n}\n\nexport interface MessageDeletedEvent extends CustomMessage {\n event: 'message.destroyed'\n data: BaseMessageEventData\n}\n"]}
1
+ {"version":3,"file":"message_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'\n\ninterface BaseMessageEventData extends Record<string, unknown> {\n data: {\n author_avatar: string\n author_id: number\n author_name: string\n conversation_id: number\n created_at: string\n deleted_at: string | null\n organization_id: number | null\n sort_key: string\n text: string\n text_edited_at: string | null\n html: string\n attachments: DenormalizedAttachmentResource[]\n idempotent_key?: string | null\n }\n}\n\nexport interface MessageCreatedEvent extends CustomMessage {\n event: 'message.created'\n data: BaseMessageEventData\n}\n\nexport interface MessageUpdatedEvent extends CustomMessage {\n event: 'message.updated'\n data: BaseMessageEventData\n}\n\nexport interface MessageDeletedEvent extends CustomMessage {\n event: 'message.destroyed'\n data: BaseMessageEventData\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Performance tracking utilities for client-side metrics (React Native)
3
+ *
4
+ * This module provides functionality to track performance metrics and send them
5
+ * to Datadog via the backend API for aggregation.
6
+ */
7
+ import { ApiClient } from '../hooks';
8
+ /**
9
+ * Start tracking message creation performance
10
+ */
11
+ export declare function startMessageCreationTracking(idempotentKey: string): void;
12
+ /**
13
+ * Complete message creation tracking when the message.created event is received
14
+ */
15
+ export declare function completeMessageCreationTracking({ apiClient, idempotentKey, }: {
16
+ apiClient: ApiClient;
17
+ idempotentKey: string;
18
+ }): void;
19
+ /**
20
+ * Complete message creation tracking when the conversation.updated event is received
21
+ */
22
+ export declare function completeMessageCreationConversationTracking({ apiClient, idempotentKey, }: {
23
+ apiClient: ApiClient;
24
+ idempotentKey: string;
25
+ }): void;
26
+ //# sourceMappingURL=performance_tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance_tracking.d.ts","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAgBpC;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAMxE;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,IAAI,CASP;AAED;;GAEG;AACH,wBAAgB,2CAA2C,CAAC,EAC1D,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,IAAI,CASP"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Performance tracking utilities for client-side metrics (React Native)
3
+ *
4
+ * This module provides functionality to track performance metrics and send them
5
+ * to Datadog via the backend API for aggregation.
6
+ */
7
+ import DeviceInfo from 'react-native-device-info';
8
+ import { Platform } from 'react-native';
9
+ const appName = DeviceInfo.getApplicationName();
10
+ const pendingMetrics = new Map();
11
+ /**
12
+ * Start tracking message creation performance
13
+ */
14
+ export function startMessageCreationTracking(idempotentKey) {
15
+ if (!idempotentKey)
16
+ return;
17
+ const startTime = Date.now();
18
+ pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime });
19
+ pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime });
20
+ cleanupStaleMetrics();
21
+ }
22
+ /**
23
+ * Complete message creation tracking when the message.created event is received
24
+ */
25
+ export function completeMessageCreationTracking({ apiClient, idempotentKey, }) {
26
+ if (!idempotentKey)
27
+ return;
28
+ const duration = durationForIdempotentKey(`message:${idempotentKey}`);
29
+ if (!duration)
30
+ return;
31
+ sendPerformanceMetric(apiClient, {
32
+ name: 'chat.message.creation.message_roundtrip_time',
33
+ value: duration,
34
+ unit: 'milliseconds',
35
+ });
36
+ }
37
+ /**
38
+ * Complete message creation tracking when the conversation.updated event is received
39
+ */
40
+ export function completeMessageCreationConversationTracking({ apiClient, idempotentKey, }) {
41
+ if (!idempotentKey)
42
+ return;
43
+ const duration = durationForIdempotentKey(`conversation:${idempotentKey}`);
44
+ if (!duration)
45
+ return;
46
+ sendPerformanceMetric(apiClient, {
47
+ name: 'chat.message.creation.conversation_roundtrip_time',
48
+ value: duration,
49
+ unit: 'milliseconds',
50
+ });
51
+ }
52
+ function durationForIdempotentKey(idempotentKey) {
53
+ const metric = pendingMetrics.get(idempotentKey);
54
+ if (!metric)
55
+ return undefined;
56
+ pendingMetrics.delete(idempotentKey);
57
+ const endTime = Date.now();
58
+ return endTime - metric.startTime;
59
+ }
60
+ /**
61
+ * Send performance metric to backend for Datadog submission
62
+ */
63
+ async function sendPerformanceMetric(apiClient, metric) {
64
+ try {
65
+ await apiClient.chat.post({
66
+ url: `/me/metrics`,
67
+ data: {
68
+ data: {
69
+ type: 'Metric',
70
+ attributes: {
71
+ metric: {
72
+ name: metric.name,
73
+ value: metric.value,
74
+ unit: metric.unit,
75
+ platform: Platform.OS,
76
+ app: appName,
77
+ },
78
+ },
79
+ },
80
+ },
81
+ });
82
+ }
83
+ catch (e) {
84
+ // Ignore errors
85
+ }
86
+ }
87
+ /**
88
+ * Clean up any stale metrics (older than 5 minutes)
89
+ * This prevents memory leaks if messages fail to complete for any reason
90
+ */
91
+ function cleanupStaleMetrics() {
92
+ const now = Date.now();
93
+ const staleThreshold = 5 * 60 * 1000; // 5 minutes in milliseconds
94
+ for (const [key, metric] of pendingMetrics.entries()) {
95
+ if (now - metric.startTime > staleThreshold) {
96
+ pendingMetrics.delete(key);
97
+ }
98
+ }
99
+ }
100
+ //# sourceMappingURL=performance_tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance_tracking.js","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,UAAU,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEvC,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AAa/C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAgC,CAAA;AAE9D;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAAC,aAAqB;IAChE,IAAI,CAAC,aAAa;QAAE,OAAM;IAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC5B,cAAc,CAAC,GAAG,CAAC,WAAW,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAA;IAC5E,cAAc,CAAC,GAAG,CAAC,gBAAgB,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAA;IACjF,mBAAmB,EAAE,CAAA;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,+BAA+B,CAAC,EAC9C,SAAS,EACT,aAAa,GAId;IACC,IAAI,CAAC,aAAa;QAAE,OAAM;IAC1B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,WAAW,aAAa,EAAE,CAAC,CAAA;IACrE,IAAI,CAAC,QAAQ;QAAE,OAAM;IACrB,qBAAqB,CAAC,SAAS,EAAE;QAC/B,IAAI,EAAE,8CAA8C;QACpD,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE,cAAc;KACrB,CAAC,CAAA;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,2CAA2C,CAAC,EAC1D,SAAS,EACT,aAAa,GAId;IACC,IAAI,CAAC,aAAa;QAAE,OAAM;IAC1B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,gBAAgB,aAAa,EAAE,CAAC,CAAA;IAC1E,IAAI,CAAC,QAAQ;QAAE,OAAM;IACrB,qBAAqB,CAAC,SAAS,EAAE;QAC/B,IAAI,EAAE,mDAAmD;QACzD,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE,cAAc;KACrB,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,wBAAwB,CAAC,aAAqB;IACrD,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;IAChD,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAC7B,cAAc,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC1B,OAAO,OAAO,GAAG,MAAM,CAAC,SAAS,CAAA;AACnC,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,qBAAqB,CAClC,SAAoB,EACpB,MAAyB;IAEzB,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;YACxB,GAAG,EAAE,aAAa;YAClB,IAAI,EAAE;gBACJ,IAAI,EAAE;oBACJ,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACV,MAAM,EAAE;4BACN,IAAI,EAAE,MAAM,CAAC,IAAI;4BACjB,KAAK,EAAE,MAAM,CAAC,KAAK;4BACnB,IAAI,EAAE,MAAM,CAAC,IAAI;4BACjB,QAAQ,EAAE,QAAQ,CAAC,EAAE;4BACrB,GAAG,EAAE,OAAO;yBACb;qBACF;iBACF;aACF;SACF,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,gBAAgB;IAClB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB;IAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,4BAA4B;IAEjE,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,cAAc,CAAC,OAAO,EAAE,EAAE,CAAC;QACrD,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,cAAc,EAAE,CAAC;YAC5C,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5B,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["/**\n * Performance tracking utilities for client-side metrics (React Native)\n *\n * This module provides functionality to track performance metrics and send them\n * to Datadog via the backend API for aggregation.\n */\n\nimport DeviceInfo from 'react-native-device-info'\nimport { Platform } from 'react-native'\nimport { ApiClient } from '../hooks'\nconst appName = DeviceInfo.getApplicationName()\n\ninterface PerformanceMetric {\n name: string\n value: number\n unit: 'milliseconds' | 'seconds' | 'count'\n}\n\ninterface PendingMessageMetric {\n idempotentKey: string\n startTime: number\n}\n\nconst pendingMetrics = new Map<string, PendingMessageMetric>()\n\n/**\n * Start tracking message creation performance\n */\nexport function startMessageCreationTracking(idempotentKey: string): void {\n if (!idempotentKey) return\n const startTime = Date.now()\n pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })\n pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })\n cleanupStaleMetrics()\n}\n\n/**\n * Complete message creation tracking when the message.created event is received\n */\nexport function completeMessageCreationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`message:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.message_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\n/**\n * Complete message creation tracking when the conversation.updated event is received\n */\nexport function completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.conversation_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\nfunction durationForIdempotentKey(idempotentKey: string): number | undefined {\n const metric = pendingMetrics.get(idempotentKey)\n if (!metric) return undefined\n pendingMetrics.delete(idempotentKey)\n const endTime = Date.now()\n return endTime - metric.startTime\n}\n\n/**\n * Send performance metric to backend for Datadog submission\n */\nasync function sendPerformanceMetric(\n apiClient: ApiClient,\n metric: PerformanceMetric\n): Promise<void> {\n try {\n await apiClient.chat.post({\n url: `/me/metrics`,\n data: {\n data: {\n type: 'Metric',\n attributes: {\n metric: {\n name: metric.name,\n value: metric.value,\n unit: metric.unit,\n platform: Platform.OS,\n app: appName,\n },\n },\n },\n },\n })\n } catch (e) {\n // Ignore errors\n }\n}\n\n/**\n * Clean up any stale metrics (older than 5 minutes)\n * This prevents memory leaks if messages fail to complete for any reason\n */\nfunction cleanupStaleMetrics(): void {\n const now = Date.now()\n const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds\n\n for (const [key, metric] of pendingMetrics.entries()) {\n if (now - metric.startTime > staleThreshold) {\n pendingMetrics.delete(key)\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.12.2-qa-328.0",
3
+ "version": "3.12.2-rc.1",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "react-native-url-polyfill": "^2.0.0",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "ab0454dcbb3eb3681caee989ffd98bd6ab3aab0a"
58
+ "gitHead": "cb8a06d84561aae9fdaf50fea7943e6b8e857067"
59
59
  }
@@ -5,6 +5,9 @@ import { useQueryClient } from '@tanstack/react-query'
5
5
  import { ApiResource, ConversationResource } from '../types'
6
6
  import { getRequestQueryKey } from './use_suspense_api'
7
7
  import { useCallback, useMemo } from 'react'
8
+ import { completeMessageCreationConversationTracking } from '../utils/performance_tracking'
9
+ import { useCurrentPerson } from './use_current_person'
10
+ import { useApiClient } from './use_api_client'
8
11
 
9
12
  interface Props {
10
13
  conversationId: number
@@ -13,14 +16,27 @@ interface Props {
13
16
  export function useConversationJoltEvents({ conversationId }: Props) {
14
17
  const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)
15
18
  const queryClient = useQueryClient()
19
+ const currentPerson = useCurrentPerson()
20
+ const apiClient = useApiClient()
16
21
  const queryKey = useMemo(
17
22
  () => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })),
18
23
  [conversationId]
19
24
  )
20
25
 
21
- const handleUpdatedConversation = useCallback(() => {
22
- queryClient.invalidateQueries({ queryKey })
23
- }, [queryClient, queryKey])
26
+ const handleUpdatedConversation = useCallback(
27
+ (e: JoltConversationEvent) => {
28
+ const { last_message_idempotent_key, last_message_author_id } = e.data.data
29
+
30
+ if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {
31
+ completeMessageCreationConversationTracking({
32
+ apiClient,
33
+ idempotentKey: last_message_idempotent_key,
34
+ })
35
+ }
36
+ queryClient.invalidateQueries({ queryKey })
37
+ },
38
+ [queryClient, queryKey, currentPerson.id, apiClient]
39
+ )
24
40
 
25
41
  const handleConversationRead = useCallback(
26
42
  (e: JoltConversationEvent) => {
@@ -15,6 +15,8 @@ import { transformReactionEventDataToReactionCountResource } from '../utils/jolt
15
15
  import { getMessagesRequestArgs } from '../utils/request/messages'
16
16
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
17
17
  import { isTemporaryMessageId } from './use_message_create_or_update'
18
+ import { completeMessageCreationTracking } from '../utils/performance_tracking'
19
+ import { useApiClient } from './use_api_client'
18
20
 
19
21
  interface Props {
20
22
  conversationId: number
@@ -26,6 +28,7 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
26
28
  const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)
27
29
  const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })
28
30
  const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)
31
+ const apiClient = useApiClient()
29
32
 
30
33
  const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =
31
34
  useTypingStatusCache(conversationId)
@@ -39,6 +42,9 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
39
42
 
40
43
  if (e.event === 'message.created' && data.author_id) {
41
44
  removeAllTypingEventsByAuthorId(data.author_id)
45
+ if (data.idempotent_key && data.author_id === currentPerson.id) {
46
+ completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key })
47
+ }
42
48
  }
43
49
 
44
50
  queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
@@ -3,16 +3,28 @@ import { ConversationRequestArgs } from '../utils/request/conversation'
3
3
  import { useConversationsCache } from './use_conversations_cache'
4
4
  import { useCurrentPerson, useCurrentPersonCache } from './use_current_person'
5
5
  import { useJoltChannel, useJoltEvent } from './use_jolt'
6
+ import { completeMessageCreationConversationTracking } from '../utils/performance_tracking'
7
+ import { useApiClient } from './use_api_client'
6
8
 
7
9
  export function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {
8
10
  const currentPerson = useCurrentPerson()
9
11
  const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
10
12
  const cache = useConversationsCache(args)
11
13
  const currentPersonCache = useCurrentPersonCache()
14
+ const apiClient = useApiClient()
12
15
 
13
- useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) =>
16
+ useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) => {
17
+ if (
18
+ e.data.data.last_message_idempotent_key &&
19
+ e.data.data.last_message_author_id === currentPerson.id
20
+ ) {
21
+ completeMessageCreationConversationTracking({
22
+ apiClient,
23
+ idempotentKey: e.data.data.last_message_idempotent_key,
24
+ })
25
+ }
14
26
  cache.fetchUpdate({ id: e.data.data.id })
15
- )
27
+ })
16
28
  useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>
17
29
  cache.fetchCreate({ id: e.data.data.id })
18
30
  )
@@ -12,6 +12,7 @@ import { DenormalizedAttachmentResourceForCreate } from '../types/resources/deno
12
12
  import { useCurrentPerson } from './use_current_person'
13
13
  import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
14
14
  import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
15
+ import { startMessageCreationTracking } from '../utils/performance_tracking'
15
16
 
16
17
  interface Props {
17
18
  conversationId: number
@@ -36,11 +37,17 @@ export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
36
37
  const fieldsWithValueJoined = Object.fromEntries(
37
38
  Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
38
39
  )
40
+ let attributes: any = { text, ...(attachments ? { attachments } : {}) }
41
+ if (!isEditing) {
42
+ const idempotentKey = insecureUUID()
43
+ attributes.idempotent_key = idempotentKey
44
+ startMessageCreationTracking(idempotentKey)
45
+ }
39
46
  const data = {
40
47
  ...requestParams.data,
41
48
  data: {
42
49
  type: 'Message',
43
- attributes: { text, ...(attachments ? { attachments } : {}) },
50
+ attributes,
44
51
  },
45
52
  fields: fieldsWithValueJoined,
46
53
  }
@@ -139,3 +146,18 @@ export function isTemporaryMessageId(messageId?: string | null): boolean {
139
146
  export function isNewMessage(message?: MessageResource): boolean {
140
147
  return !message?.id || isTemporaryMessageId(message.id)
141
148
  }
149
+
150
+ /**
151
+ * Generate a random UUID (v4) for idempotent keys.
152
+ * Uses Math.random, which is not cryptographically secure.
153
+ * An actual crypto library requires native dependencies.
154
+ * This is OK for now since idempotent keys are not security-sensitive
155
+ * or need to be guaranteed unique.
156
+ * They are short lived and we use it in combination with the message's creator_id so
157
+ * their impact is scoped only the current user.
158
+ */
159
+ function insecureUUID(): string {
160
+ return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'
161
+ .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))
162
+ .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))
163
+ }
@@ -10,6 +10,7 @@ interface BaseConversationEventData extends Record<string, unknown> {
10
10
  last_message_author_name?: string
11
11
  last_message_created_at?: DateString
12
12
  last_message_sort_key?: string
13
+ last_message_idempotent_key?: string | null
13
14
  last_message_text_preview?: string
14
15
  latest_read_message_sort_key?: string
15
16
  organization_id: number
@@ -15,6 +15,7 @@ interface BaseMessageEventData extends Record<string, unknown> {
15
15
  text_edited_at: string | null
16
16
  html: string
17
17
  attachments: DenormalizedAttachmentResource[]
18
+ idempotent_key?: string | null
18
19
  }
19
20
  }
20
21
 
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Performance tracking utilities for client-side metrics (React Native)
3
+ *
4
+ * This module provides functionality to track performance metrics and send them
5
+ * to Datadog via the backend API for aggregation.
6
+ */
7
+
8
+ import DeviceInfo from 'react-native-device-info'
9
+ import { Platform } from 'react-native'
10
+ import { ApiClient } from '../hooks'
11
+ const appName = DeviceInfo.getApplicationName()
12
+
13
+ interface PerformanceMetric {
14
+ name: string
15
+ value: number
16
+ unit: 'milliseconds' | 'seconds' | 'count'
17
+ }
18
+
19
+ interface PendingMessageMetric {
20
+ idempotentKey: string
21
+ startTime: number
22
+ }
23
+
24
+ const pendingMetrics = new Map<string, PendingMessageMetric>()
25
+
26
+ /**
27
+ * Start tracking message creation performance
28
+ */
29
+ export function startMessageCreationTracking(idempotentKey: string): void {
30
+ if (!idempotentKey) return
31
+ const startTime = Date.now()
32
+ pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })
33
+ pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })
34
+ cleanupStaleMetrics()
35
+ }
36
+
37
+ /**
38
+ * Complete message creation tracking when the message.created event is received
39
+ */
40
+ export function completeMessageCreationTracking({
41
+ apiClient,
42
+ idempotentKey,
43
+ }: {
44
+ apiClient: ApiClient
45
+ idempotentKey: string
46
+ }): void {
47
+ if (!idempotentKey) return
48
+ const duration = durationForIdempotentKey(`message:${idempotentKey}`)
49
+ if (!duration) return
50
+ sendPerformanceMetric(apiClient, {
51
+ name: 'chat.message.creation.message_roundtrip_time',
52
+ value: duration,
53
+ unit: 'milliseconds',
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Complete message creation tracking when the conversation.updated event is received
59
+ */
60
+ export function completeMessageCreationConversationTracking({
61
+ apiClient,
62
+ idempotentKey,
63
+ }: {
64
+ apiClient: ApiClient
65
+ idempotentKey: string
66
+ }): void {
67
+ if (!idempotentKey) return
68
+ const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)
69
+ if (!duration) return
70
+ sendPerformanceMetric(apiClient, {
71
+ name: 'chat.message.creation.conversation_roundtrip_time',
72
+ value: duration,
73
+ unit: 'milliseconds',
74
+ })
75
+ }
76
+
77
+ function durationForIdempotentKey(idempotentKey: string): number | undefined {
78
+ const metric = pendingMetrics.get(idempotentKey)
79
+ if (!metric) return undefined
80
+ pendingMetrics.delete(idempotentKey)
81
+ const endTime = Date.now()
82
+ return endTime - metric.startTime
83
+ }
84
+
85
+ /**
86
+ * Send performance metric to backend for Datadog submission
87
+ */
88
+ async function sendPerformanceMetric(
89
+ apiClient: ApiClient,
90
+ metric: PerformanceMetric
91
+ ): Promise<void> {
92
+ try {
93
+ await apiClient.chat.post({
94
+ url: `/me/metrics`,
95
+ data: {
96
+ data: {
97
+ type: 'Metric',
98
+ attributes: {
99
+ metric: {
100
+ name: metric.name,
101
+ value: metric.value,
102
+ unit: metric.unit,
103
+ platform: Platform.OS,
104
+ app: appName,
105
+ },
106
+ },
107
+ },
108
+ },
109
+ })
110
+ } catch (e) {
111
+ // Ignore errors
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Clean up any stale metrics (older than 5 minutes)
117
+ * This prevents memory leaks if messages fail to complete for any reason
118
+ */
119
+ function cleanupStaleMetrics(): void {
120
+ const now = Date.now()
121
+ const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds
122
+
123
+ for (const [key, metric] of pendingMetrics.entries()) {
124
+ if (now - metric.startTime > staleThreshold) {
125
+ pendingMetrics.delete(key)
126
+ }
127
+ }
128
+ }