• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

ringcentral / ringcentral-js-widgets / 21017357600

15 Jan 2026 02:19AM UTC coverage: 61.994% (-1.1%) from 63.06%
21017357600

push

github

web-flow
misc: sync features and bugfixes from 8f9fbb5dfe (#1784)

* misc: sync crius

* chore: update babel-setting deps

* feat(core): update logger and add fromWatch

* misc: fix tsconfig

* misc: fix tsconfig

* misc: update eslint-settings and new eslint-plugin-crius package

* chore: remove ci and nx from package.json

* feat(i18n): new getAcceptLocaleMap and preudo string support

* chore: add preudo i18n

* misc(locale-loader): convert to ts, new format support

* misc(locale-settings): use ts

* chore: add format test in phone number lib

* feat(react-hooks): add more hooks

* chore: add comments

* misc: add more mock files

* misc: update test utils

* misc: update utils

* chore: update tsconfig

* misc: update i18n string, and convert to ts

* feat: update ui components, support emoji input, new video setting ui

* feat: new rcvideo v2 module

* feat: use new subscription register api

* misc(commons): update enums/interfaces

* misc: update Analytics lib

* misc: upgrade uuid and update import

* misc(commons): update formatDuration lib

* misc(test): add test steps

* chore: update tests and more feature tests

* misc: update demo project

* misc: update cli template

* misc: fix deps issue

* misc: remove glip widgets package

* misc: fix wrong import path

* misc: limit jest worker memory

* chore: use npm trusted-publishers

10285 of 18150 branches covered (56.67%)

Branch coverage included in aggregate %.

986 of 2186 new or added lines in 228 files covered. (45.11%)

44 existing lines in 23 files now uncovered.

17404 of 26514 relevant lines covered (65.64%)

167640.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.0
/packages/utils/src/utils/createRepeatTrackingManager.ts
1
import chunk from 'lodash/chunk';
2
import {
3
  BehaviorSubject,
4
  buffer,
5
  combineLatest,
6
  distinctUntilChanged,
7
  EMPTY,
8
  filter,
9
  identity,
10
  map,
11
  type Observable,
12
  of,
13
  repeat,
14
  scan,
15
  startWith,
16
  Subject,
17
  switchMap,
18
  take,
19
  tap,
20
  throttleTime,
21
} from 'rxjs';
22

23
export type RepeatTrackingItems = readonly [string, string[]];
24

25
export type CreateRepeatTrackingManagerOptions<T extends Record<any, any>> = {
26
  ttl: number | 'never';
27
  groupKey: keyof T;
28
  itemKey: keyof T;
29
  sendToServer: (data: RepeatTrackingItems[]) => Promise<void>;
30
  validate?: (data: T) => boolean;
31
  maxBatchRequestCount?: number;
32
};
33

34
/**
35
 * Creates a manager for repeat tracking of viewable items which linking.
36
 *
37
 * When the items be linked, will interval emit to the fromServerListener api that will provide you able to know what items still on the screen
38
 *
39
 * and the linked api trigger will have a throttle time to prevent the api be triggered too often
40
 *
41
 * @template T - Generic type extending Record<string, string> representing the data structure
42
 *
43
 * @param options - Configuration options object
44
 * @param options.sendToServer - Function to send data to server
45
 * @param options.ttl - Time-to-live duration in milliseconds for cache entries
46
 * @param options.groupKey - Key used to group items
47
 * @param options.itemKey - Key used to identify individual items
48
 * @param options.maxBatchRequestCount - Maximum number of items per batch request (defaults to Number.MAX_SAFE_INTEGER)
49
 * @param options.validate - Function to validate data items
50
 *
51
 * @returns An object containing methods to manage viewable items:
52
 * - link: Adds a new item to be tracked
53
 * - unlink: Removes an item from tracking
54
 * - setListenerDataFromClient: Updates data for a specific client
55
 * - fromClientListener: Creates an observable for client-side changes
56
 * - fromServerListener: Creates an observable for server-side async event to fetch data and return the cache success list
57
 * - clear: Resets all tracking data
58
 *
59
 * @example
60
 * const manager = createRepeatTrackingManager({
61
 *   sendToServer: async (data) => { ... },
62
 *   ttl: 30000,
63
 *   groupKey: 'accountId',
64
 *   itemKey: 'extensionId',
65
 *   validate: (data) => true
66
 * });
67
 */
68
export const createRepeatTrackingManager = <T extends Record<any, any>>({
304✔
69
  sendToServer,
70
  ttl,
71
  groupKey,
72
  itemKey,
73
  maxBatchRequestCount = Number.MAX_SAFE_INTEGER,
6✔
74
  validate,
75
}: CreateRepeatTrackingManagerOptions<T>) => {
76
  const link$ = new Subject<T>();
7✔
77
  const unlink$ = new Subject<T>();
7✔
78
  const clientsLinkedItemsMap$ = new BehaviorSubject(
7✔
79
    new Map<string, RepeatTrackingItems[]>(),
80
  );
81
  const isNever = ttl === 'never';
7✔
82

83
  const cacheMap = new Map<string, number>();
7✔
84

85
  const fromClientListener = () => {
7✔
86
    const everLinkedMap$ = link$.pipe(
2✔
87
      buffer(
88
        // when got first value, buffer 1s data to fetch all presence in once
89
        link$.pipe(
90
          throttleTime(1000, undefined, {
91
            leading: false,
92
            trailing: true,
93
          }),
94
        ),
95
      ),
96
      // accumulate all linked presence
97
      scan((distinctMap, list) => {
98
        // distinct the list by accountId and extensionId
99
        list.forEach((item) => {
2✔
100
          const groupValue = item[groupKey];
4✔
101
          const itemValue = item[itemKey];
4✔
102

103
          if (validate ? validate(item) : true) {
4!
104
            const set = distinctMap.get(groupValue) ?? new Set<string>();
4✔
105
            set.add(itemValue);
4✔
106
            distinctMap.set(groupValue, set);
4✔
107
          }
108
        });
109

110
        return distinctMap;
2✔
111
      }, new Map<string, Set<string>>()),
112
    );
113

114
    const unlinkBuffer$ = unlink$.pipe(
2✔
115
      buffer(
116
        unlink$.pipe(
117
          // 5000ms throttle for more time is fine, can later remove subscribe
118
          throttleTime(5000, undefined, {
119
            leading: false,
120
            trailing: true,
121
          }),
122
        ),
123
      ),
124
      startWith(null),
125
    );
126

127
    return combineLatest([
2✔
128
      everLinkedMap$,
129
      unlinkBuffer$.pipe(
130
        take(2),
131
        // once emit, then restart the buffer, so will only use the buffer data once, then clear
132
        repeat(),
133
      ),
134
    ]).pipe(
135
      map(([everLinkedMap, unlinkList]) => {
136
        if (unlinkList) {
6✔
137
          unlinkList.forEach((item) => {
2✔
138
            const groupValue = item[groupKey];
2✔
139
            const itemValue = item[itemKey];
2✔
140
            const set = everLinkedMap.get(groupValue);
2✔
141

142
            if (set) {
2!
143
              set.delete(itemValue);
2✔
144

145
              if (set.size === 0) {
2✔
146
                everLinkedMap.delete(groupValue);
1✔
147
              }
148
            }
149
          });
150
        }
151

152
        return everLinkedMap;
6✔
153
      }),
154
      // convert to serializable list
155
      map((distinctMap) => {
156
        const list = Array.from(distinctMap).map(([groupValue, items]) => {
6✔
157
          const itemList = Array.from(items);
4✔
158

159
          return [groupValue, itemList] as const;
4✔
160
        });
161

162
        return list;
6✔
163
      }),
164
      // only emit when the list is changed
165
      distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
4✔
166
      switchMap((data) => sendToServer(data)),
4✔
167
    );
168
  };
169

170
  const fromServerListener = (
7✔
171
    sendRequest: (
172
      data: [string, string[]][],
173
    ) => Promise<string[]> | Observable<string[]>,
174
  ) => {
175
    // the list from clients
176
    return clientsLinkedItemsMap$.pipe(
4✔
177
      // distinct the list by first key
178
      map((linkedItems) => {
179
        const distinctMap = new Map<string, Set<string>>();
4✔
180

181
        linkedItems.forEach((child) => {
4✔
182
          child.forEach((val) => {
3✔
183
            const [groupValue, items] = val;
3✔
184
            const set = distinctMap.get(groupValue) ?? new Set<string>();
3✔
185
            items.forEach((id) => set.add(id));
6✔
186
            distinctMap.set(groupValue, set);
3✔
187
          });
188
        });
189

190
        return Array.from(distinctMap.entries());
4✔
191
      }),
192
      // if the list is not empty, emit the expired presence list every ttl
193
      switchMap((list) => {
194
        return list.length > 0
4✔
195
          ? of(null).pipe(
196
              map(() =>
197
                list.reduce((acc, [groupValue, itemSet]) => {
4✔
198
                  const validExtensionIds = Array.from(itemSet).filter(
4✔
199
                    (item) => {
200
                      const prevTimestamp = cacheMap.get(item);
7✔
201

202
                      return (
7✔
203
                        isNever ||
15✔
204
                        !prevTimestamp ||
205
                        Date.now() - prevTimestamp >= ttl
206
                      );
207
                    },
208
                  );
209

210
                  if (validExtensionIds.length > 0) {
4!
211
                    if (validExtensionIds.length > maxBatchRequestCount) {
4✔
212
                      const splitItems = chunk(
1✔
213
                        validExtensionIds,
214
                        maxBatchRequestCount,
215
                      );
216

217
                      splitItems.forEach((splitItem) => {
1✔
218
                        acc.push([groupValue, splitItem]);
2✔
219
                      });
220
                    } else {
221
                      acc.push([groupValue, validExtensionIds]);
3✔
222
                    }
223
                  }
224
                  return acc;
4✔
225
                }, [] as [string, string[]][]),
226
              ),
227
              filter((list) => list.length > 0),
4✔
228
              switchMap((data) => sendRequest(data)),
4✔
229
              tap((cacheList) => {
230
                if (isNever) return;
1!
231
                // the minus 1ms to make sure the cache is expired in the next cycle
232
                const successTime = Date.now() - 1;
1✔
233

234
                cacheList.forEach((cacheItem) => {
1✔
235
                  cacheMap.set(cacheItem, successTime);
1✔
236
                });
237
              }),
238
              isNever ? identity : repeat({ delay: ttl }),
3!
239
            )
240
          : EMPTY;
241
      }),
242
    );
243
  };
244

245
  const clear = () => {
7✔
246
    clientsLinkedItemsMap$.next(new Map());
1✔
247
    cacheMap.clear();
1✔
248
  };
249

250
  return {
7✔
251
    link(data: T) {
252
      link$.next(data);
4✔
253
    },
254
    unlink(data: T) {
255
      unlink$.next(data);
2✔
256
    },
257
    setListenerDataFromClient: (
258
      clientId: string,
259
      data: RepeatTrackingItems[],
260
    ) => {
261
      const presenceMap = clientsLinkedItemsMap$.value;
4✔
262
      presenceMap.set(clientId, data);
4✔
263
      clientsLinkedItemsMap$.next(presenceMap);
4✔
264
    },
265
    fromClientListener,
266
    fromServerListener,
267
    clear,
268
    get clientsLinkedItemsMap() {
NEW
269
      return clientsLinkedItemsMap$.value;
×
270
    },
271
  };
272
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc