querysub 0.394.0 → 0.395.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.
Files changed (30) hide show
  1. package/.cursorrules +8 -0
  2. package/package.json +1 -1
  3. package/src/-a-archives/archivesJSONT.ts +71 -8
  4. package/src/0-path-value-core/pathValueCore.ts +20 -1
  5. package/src/5-diagnostics/GenericFormat.tsx +1 -1
  6. package/src/deployManager/components/MachinesListPage.tsx +1 -2
  7. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +12 -3
  8. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +8 -3
  9. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +24 -9
  10. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +0 -1
  11. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +21 -5
  12. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +10 -4
  13. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +95 -124
  14. package/src/diagnostics/logs/IndexedLogs/RenderSearchStats.tsx +127 -0
  15. package/src/diagnostics/logs/IndexedLogs/bufferSearchFindMatcher.ts +3 -0
  16. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
  17. package/src/diagnostics/logs/TimeRangeSelector.tsx +11 -2
  18. package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -4
  19. package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
  20. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +946 -0
  21. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +49 -0
  22. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +553 -0
  23. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +125 -90
  24. package/src/diagnostics/managementPages.tsx +17 -1
  25. package/src/functional/{limitProcessing.ts → throttleProcessing.ts} +1 -1
  26. package/src/library-components/StartEllipsis.tsx +13 -0
  27. package/src/misc.ts +4 -0
  28. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +0 -106
  29. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +0 -2
  30. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +0 -5
@@ -0,0 +1,49 @@
1
+ import { LifeCycle, LifeCycleEntry } from "./lifeCycles";
2
+ import { LogDatum } from "../diskLogger";
3
+ import { createMatchesPatternCached } from "../IndexedLogs/bufferSearchFindMatcher";
4
+
5
+ export type LifecycleMatch = {
6
+ lifecycle: LifeCycle;
7
+ entry: LifeCycleEntry;
8
+ datum: Record<string, unknown>;
9
+ };
10
+
11
+ export function getLifecycleMatchesForDatum(
12
+ lifecycles: LifeCycle[],
13
+ datum: LogDatum
14
+ ): LifecycleMatch[] {
15
+ let matches: LifecycleMatch[] = [];
16
+
17
+ let matchStr = String(datum.param0);
18
+ let matchBuffer = Buffer.from(matchStr, "utf8");
19
+
20
+ for (let lifecycle of lifecycles) {
21
+ for (let entry of lifecycle.entries) {
22
+ let matcher = createMatchesPatternCached(entry.matchPattern);
23
+ if (!matcher(matchBuffer)) continue;
24
+
25
+ let extractedDatum: Record<string, unknown> = {};
26
+ let hasAllKeys = true;
27
+
28
+ for (let keyConfig of entry.groupByKeys) {
29
+ let keyToLookup = keyConfig.startKey || keyConfig.ourKey;
30
+ let value = datum[keyToLookup];
31
+ if (value === undefined) {
32
+ hasAllKeys = false;
33
+ break;
34
+ }
35
+ extractedDatum[keyConfig.ourKey] = value;
36
+ }
37
+
38
+ if (!hasAllKeys || entry.groupByKeys.length === 0) continue;
39
+
40
+ matches.push({
41
+ lifecycle,
42
+ entry,
43
+ datum: extractedDatum,
44
+ });
45
+ }
46
+ }
47
+
48
+ return matches;
49
+ }
@@ -0,0 +1,553 @@
1
+ import { Querysub } from "../../../4-querysub/QuerysubController";
2
+ import { getTimeRange } from "../TimeRangeSelector";
3
+ import { URLParam } from "../../../library-components/URLParam";
4
+ import { throttleFunction, sort } from "socket-function/src/misc";
5
+ import { getLoggers2Async, LogDatum } from "../diskLogger";
6
+ import { IndexedLogResults, createEmptyIndexedLogResults, mergeIndexedLogResults } from "../IndexedLogs/BufferIndexHelpers";
7
+ import { errorToUndefined } from "../../../errors";
8
+ import { unique, maybeUndefined } from "../../../misc";
9
+ import { formatSearchString } from "../IndexedLogs/LogViewerParams";
10
+ import { createMatchesPatternCached } from "../IndexedLogs/bufferSearchFindMatcher";
11
+ import { getPathStr } from "../../../path";
12
+ import { LifeCycle, LifeCycleEntry, LifeCyclesController } from "./lifeCycles";
13
+
14
+ export let limitURL = new URLParam("lifecyclelimit", 100);
15
+ export let additionalSearchURL = new URLParam("lifecyclesearch", "");
16
+
17
+ type LifecycleInstance = {
18
+ key: string;
19
+ keys: { key: string; value: unknown }[];
20
+ entries: {
21
+ datum: LogDatum;
22
+ matchPattern: string;
23
+ }[];
24
+ isComplete?: boolean;
25
+ isTruncated?: boolean;
26
+ isWarning?: boolean;
27
+ warningMessage?: string;
28
+ startTime: number;
29
+ endTime?: number;
30
+ };
31
+
32
+ export function createLifeCycleSearch(
33
+ context: {
34
+ controller: ReturnType<typeof LifeCyclesController>;
35
+ state: {
36
+ searchingLifeCycleId: string | undefined;
37
+ phase1Results: LogDatum[];
38
+ phase1InvalidResults: Array<{ datum: LogDatum; missingKeys: string[] }>;
39
+ phase1Stats: IndexedLogResults | undefined;
40
+ phase1Searching: boolean;
41
+ phase2Results: LogDatum[];
42
+ phase2Stats: IndexedLogResults | undefined;
43
+ phase2Searching: boolean;
44
+ lifecycleInstances: LifecycleInstance[];
45
+ };
46
+ }
47
+ ) {
48
+ let searchSequenceNumber = 0;
49
+
50
+ let cancel = async () => {
51
+ searchSequenceNumber++;
52
+ let loggers = await getLoggers2Async();
53
+ for (let logger of Object.values(loggers)) {
54
+ logger.clientCancelAllCallbacks();
55
+ }
56
+ Querysub.commitLocal(() => {
57
+ context.state.phase1Searching = false;
58
+ context.state.phase2Searching = false;
59
+ });
60
+ };
61
+
62
+ // Entry point for lifecycle search. Currently only supports "find all" mode by passing empty seedDatums.
63
+ // Future: Will be parameterized to accept specific log datums from other pages.
64
+ let searchLifeCycle = async (lifeCycleId: string) => {
65
+ let lifeCycles = await context.controller.getLifeCycles.promise();
66
+ if (!lifeCycles) return;
67
+
68
+ let lifeCycle = lifeCycles.find(lc => lc.id === lifeCycleId);
69
+ if (!lifeCycle) return;
70
+
71
+ if (lifeCycle.entries.length === 0) {
72
+ return;
73
+ }
74
+
75
+ await cancel();
76
+
77
+ Querysub.commitLocal(() => {
78
+ context.state.searchingLifeCycleId = lifeCycleId;
79
+ context.state.phase1Results = [];
80
+ context.state.phase1InvalidResults = [];
81
+ context.state.phase1Stats = undefined;
82
+ context.state.phase2Results = [];
83
+ context.state.phase2Stats = undefined;
84
+ context.state.lifecycleInstances = [];
85
+ });
86
+
87
+ searchSequenceNumber++;
88
+ let currentSequenceNumber = searchSequenceNumber;
89
+
90
+ let phase1Results = await searchPhase1({
91
+ lifeCycle,
92
+ currentSequenceNumber,
93
+ });
94
+
95
+ if (searchSequenceNumber !== currentSequenceNumber) return;
96
+
97
+ if (phase1Results.length === 0) {
98
+ return;
99
+ }
100
+
101
+ let phase2Data = await searchPhase2({
102
+ lifeCycle,
103
+ phase1Results,
104
+ currentSequenceNumber,
105
+ });
106
+
107
+ if (searchSequenceNumber !== currentSequenceNumber) return;
108
+
109
+ let range = Querysub.localRead(() => getTimeRange());
110
+
111
+ let instances = groupLifecycleResults({
112
+ phase2Results: phase2Data.results,
113
+ lifeCycle,
114
+ searchEndTime: range.endTime,
115
+ hitLimit: phase2Data.hitLimit,
116
+ });
117
+
118
+ Querysub.commitLocal(() => {
119
+ context.state.lifecycleInstances = instances;
120
+ });
121
+ };
122
+
123
+ let searchPhase1 = async (params: {
124
+ lifeCycle: LifeCycle;
125
+ currentSequenceNumber: number;
126
+ }) => {
127
+ let { lifeCycle, currentSequenceNumber } = params;
128
+ let startEntries = lifeCycle.entries.filter(e => e.isStart);
129
+
130
+ if (startEntries.length === 0) {
131
+ return [];
132
+ }
133
+
134
+ Querysub.commitLocal(() => {
135
+ context.state.phase1Searching = true;
136
+ context.state.phase1Results = [];
137
+ context.state.phase1InvalidResults = [];
138
+ context.state.phase1Stats = undefined;
139
+ });
140
+
141
+ let searchQuery = startEntries.map(entry => entry.matchPattern).join("|");
142
+ let additionalSearch = additionalSearchURL.value.trim();
143
+ if (additionalSearch) {
144
+ searchQuery = searchQuery + "&" + additionalSearch;
145
+ }
146
+ let searchBuffer = Buffer.from(searchQuery, "utf8");
147
+
148
+ let sourceTypes = new Set<string>();
149
+ for (let entry of startEntries) {
150
+ sourceTypes.add(entry.sourceType);
151
+ }
152
+
153
+ let loggers = await getLoggers2Async();
154
+ let selectedLoggers: typeof loggers.logLogs[] = [];
155
+
156
+ if (sourceTypes.has("log")) selectedLoggers.push(loggers.logLogs);
157
+ if (sourceTypes.has("info")) selectedLoggers.push(loggers.infoLogs);
158
+ if (sourceTypes.has("warning")) selectedLoggers.push(loggers.warnLogs);
159
+ if (sourceTypes.has("error")) selectedLoggers.push(loggers.errorLogs);
160
+
161
+ if (selectedLoggers.length === 0) {
162
+ Querysub.commitLocal(() => {
163
+ context.state.phase1Searching = false;
164
+ });
165
+ return [];
166
+ }
167
+
168
+ let startTime = Date.now();
169
+ let results: LogDatum[] = [];
170
+
171
+ let range = Querysub.localRead(() => getTimeRange());
172
+
173
+ let updateResults = throttleFunction(100, () => {
174
+ if (searchSequenceNumber !== currentSequenceNumber) return;
175
+ Querysub.commitLocal(() => {
176
+ context.state.phase1Results = results;
177
+ });
178
+ });
179
+
180
+ let loggerResults = new Map<typeof loggers.logLogs, IndexedLogResults>();
181
+
182
+ let updateStats = () => {
183
+ if (searchSequenceNumber !== currentSequenceNumber) return;
184
+
185
+ let mergedStats: IndexedLogResults = createEmptyIndexedLogResults();
186
+
187
+ for (let loggerResult of loggerResults.values()) {
188
+ mergedStats = mergeIndexedLogResults(mergedStats, loggerResult);
189
+ }
190
+
191
+ mergedStats.totalSearchTime = Date.now() - startTime;
192
+
193
+ Querysub.commitLocal(() => {
194
+ context.state.phase1Stats = mergedStats;
195
+ });
196
+ };
197
+
198
+ await Promise.all(selectedLoggers.map(async (logger) => {
199
+ let done = false;
200
+ let result = await errorToUndefined(logger.clientFind({
201
+ params: {
202
+ startTime: range.startTime,
203
+ endTime: range.endTime,
204
+ limit: limitURL.value,
205
+ findBuffer: searchBuffer,
206
+ searchFromStart: range.searchFromStart,
207
+ },
208
+ onResult: (match: LogDatum) => {
209
+ results.push(match);
210
+ void updateResults();
211
+ },
212
+ onResults: (loggerStats: IndexedLogResults) => {
213
+ if (done) return;
214
+ loggerResults.set(logger, loggerStats);
215
+ updateStats();
216
+ },
217
+ }));
218
+ done = true;
219
+
220
+ if (result) {
221
+ loggerResults.set(logger, result);
222
+ updateStats();
223
+ }
224
+ }));
225
+
226
+ let validResults: LogDatum[] = [];
227
+ let invalidResults: Array<{ datum: LogDatum; missingKeys: string[] }> = [];
228
+
229
+ for (let datum of results) {
230
+ let matchStr = String(datum.param0);
231
+ let matchBuffer = Buffer.from(matchStr, "utf8");
232
+ let matchedEntry: LifeCycleEntry | undefined = undefined;
233
+
234
+ for (let startEntry of startEntries) {
235
+ let matcher = createMatchesPatternCached(startEntry.matchPattern);
236
+ if (matcher(matchBuffer)) {
237
+ matchedEntry = startEntry;
238
+ break;
239
+ }
240
+ }
241
+
242
+ if (!matchedEntry) {
243
+ validResults.push(datum);
244
+ continue;
245
+ }
246
+
247
+ let missingKeys: string[] = [];
248
+ for (let keyConfig of matchedEntry.groupByKeys) {
249
+ let value = datum[keyConfig.ourKey];
250
+ if (value === undefined) {
251
+ missingKeys.push(keyConfig.ourKey);
252
+ }
253
+ }
254
+
255
+ if (missingKeys.length > 0) {
256
+ invalidResults.push({ datum, missingKeys });
257
+ } else {
258
+ validResults.push(datum);
259
+ }
260
+ }
261
+
262
+ Querysub.commitLocal(() => {
263
+ context.state.phase1Results = validResults;
264
+ context.state.phase1InvalidResults = invalidResults;
265
+ context.state.phase1Searching = false;
266
+ });
267
+
268
+ return validResults;
269
+ };
270
+
271
+ let searchPhase2 = async (params: {
272
+ lifeCycle: LifeCycle;
273
+ phase1Results: LogDatum[];
274
+ currentSequenceNumber: number;
275
+ }): Promise<{ results: LogDatum[]; hitLimit: boolean }> => {
276
+ let { lifeCycle, phase1Results, currentSequenceNumber } = params;
277
+
278
+ if (phase1Results.length === 0) {
279
+ return { results: [], hitLimit: false };
280
+ }
281
+
282
+ Querysub.commitLocal(() => {
283
+ context.state.phase2Searching = true;
284
+ context.state.phase2Results = [];
285
+ context.state.phase2Stats = undefined;
286
+ });
287
+
288
+ let allResults: LogDatum[] = [];
289
+ let startTime = Date.now();
290
+ let loggers = await getLoggers2Async();
291
+
292
+ let updateResults = throttleFunction(100, () => {
293
+ if (searchSequenceNumber !== currentSequenceNumber) return;
294
+ Querysub.commitLocal(() => {
295
+ context.state.phase2Results = allResults;
296
+ });
297
+ });
298
+
299
+ let loggerResults = new Map<typeof loggers.logLogs, IndexedLogResults>();
300
+
301
+ let updateStats = () => {
302
+ if (searchSequenceNumber !== currentSequenceNumber) return;
303
+
304
+ let mergedStats: IndexedLogResults = createEmptyIndexedLogResults();
305
+
306
+ for (let loggerResult of loggerResults.values()) {
307
+ mergedStats = mergeIndexedLogResults(mergedStats, loggerResult);
308
+ }
309
+
310
+ mergedStats.totalSearchTime = Date.now() - startTime;
311
+
312
+ Querysub.commitLocal(() => {
313
+ context.state.phase2Stats = mergedStats;
314
+ });
315
+ };
316
+
317
+ let range = Querysub.localRead(() => getTimeRange());
318
+
319
+ let startEntries = lifeCycle.entries.filter(e => e.isStart);
320
+ if (startEntries.length === 0) {
321
+ Querysub.commitLocal(() => {
322
+ context.state.phase2Searching = false;
323
+ });
324
+ return { results: [], hitLimit: false };
325
+ }
326
+
327
+ let searchesByLogger = new Map<string, Set<string>>();
328
+
329
+ for (let datum of phase1Results) {
330
+ let matchStr = String(datum.param0);
331
+ let matchBuffer = Buffer.from(matchStr, "utf8");
332
+
333
+ for (let startEntry of startEntries) {
334
+ let matcher = createMatchesPatternCached(startEntry.matchPattern);
335
+ if (!matcher(matchBuffer)) continue;
336
+
337
+ if (startEntry.groupByKeys.length === 0) continue;
338
+
339
+ // TODO: For most of these, only the first entry is unique, so we should really create a cache to be able to know all of the unique keys that we need to search for for each value. However, it probably won't matter, as we're not going to have that many results we're searching for.
340
+ for (let entry of lifeCycle.entries) {
341
+ if (entry.groupByKeys.length === 0) continue;
342
+
343
+ let queries = searchesByLogger.get(entry.sourceType);
344
+ if (!queries) {
345
+ queries = new Set<string>();
346
+ searchesByLogger.set(entry.sourceType, queries);
347
+ }
348
+
349
+ let queryPart: Record<string, unknown> = {};
350
+ let matchedAll = true;
351
+
352
+ for (let startKeyObj of startEntry.groupByKeys) {
353
+ let remappedKey = entry.groupByKeys.find(k => (k.startKey || k.ourKey) === startKeyObj.ourKey)?.ourKey;
354
+ if (!remappedKey) {
355
+ matchedAll = false;
356
+ break;
357
+ }
358
+ let value = datum[startKeyObj.ourKey];
359
+ if (value === undefined) {
360
+ matchedAll = false;
361
+ break;
362
+ }
363
+ queryPart[remappedKey] = value;
364
+ }
365
+
366
+ if (matchedAll) {
367
+ queries.add(formatSearchString(queryPart));
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ if (searchesByLogger.size === 0) {
374
+ Querysub.commitLocal(() => {
375
+ context.state.phase2Searching = false;
376
+ });
377
+ return { results: [], hitLimit: false };
378
+ }
379
+
380
+ let anyHitLimit = false;
381
+
382
+ await Promise.all(Array.from(searchesByLogger.entries()).map(async ([sourceType, queries]) => {
383
+ let logger =
384
+ sourceType === "log" && loggers.logLogs ||
385
+ sourceType === "info" && loggers.infoLogs ||
386
+ sourceType === "warning" && loggers.warnLogs ||
387
+ loggers.errorLogs;
388
+
389
+ let searchQuery = Array.from(queries).join("|");
390
+ let searchBuffer = Buffer.from(searchQuery, "utf8");
391
+
392
+ let results: LogDatum[] = [];
393
+ let done = false;
394
+ let searchLimit = limitURL.value * lifeCycle.entries.length * queries.size * 3;
395
+ let result = await errorToUndefined(logger.clientFind({
396
+ params: {
397
+ startTime: range.startTime,
398
+ endTime: range.endTime,
399
+ limit: searchLimit,
400
+ findBuffer: searchBuffer,
401
+ searchFromStart: range.searchFromStart,
402
+ },
403
+ onResult: (match: LogDatum) => {
404
+ results.push(match);
405
+ allResults.push(match);
406
+ void updateResults();
407
+ },
408
+ onResults: (loggerStats: IndexedLogResults) => {
409
+ if (done) return;
410
+ loggerResults.set(logger, loggerStats);
411
+ updateStats();
412
+ },
413
+ }));
414
+ done = true;
415
+
416
+ if (result) {
417
+ loggerResults.set(logger, result);
418
+ updateStats();
419
+ }
420
+
421
+ if (results.length >= searchLimit) {
422
+ anyHitLimit = true;
423
+ }
424
+ }));
425
+
426
+ Querysub.commitLocal(() => {
427
+ context.state.phase2Results = allResults;
428
+ context.state.phase2Searching = false;
429
+ });
430
+
431
+ return { results: allResults, hitLimit: anyHitLimit };
432
+ };
433
+
434
+ let groupLifecycleResults = (params: {
435
+ phase2Results: LogDatum[];
436
+ lifeCycle: LifeCycle;
437
+ searchEndTime: number;
438
+ hitLimit: boolean;
439
+ }): LifecycleInstance[] => {
440
+ let { phase2Results, lifeCycle, searchEndTime, hitLimit } = params;
441
+
442
+ let startEntries = lifeCycle.entries.filter(e => e.isStart);
443
+ if (startEntries.length === 0) return [];
444
+
445
+ let keyFields = startEntries[0].groupByKeys;
446
+ if (keyFields.length === 0) return [];
447
+
448
+ let hasExplicitEnd = lifeCycle.entries.some(e => e.isEnd);
449
+
450
+ sort(phase2Results, x => x.time);
451
+
452
+ let instancesByKey: Map<string, LifecycleInstance[]> = new Map();
453
+
454
+ for (let datum of phase2Results) {
455
+ let keyValuePairs: { key: string; value: unknown }[] = [];
456
+ let hasAllKeys = true;
457
+ for (let keyConfig of keyFields) {
458
+ let value = datum[keyConfig.ourKey];
459
+ if (value === undefined) {
460
+ hasAllKeys = false;
461
+ break;
462
+ }
463
+ keyValuePairs.push({ key: keyConfig.ourKey, value });
464
+ }
465
+ if (!hasAllKeys) continue;
466
+ let compositeKey = getPathStr(keyValuePairs.flatMap(kv => [kv.key, String(kv.value)]));
467
+
468
+ let matchStr = String(datum.param0);
469
+ let matchBuffer = Buffer.from(matchStr, "utf8");
470
+
471
+ for (let entry of lifeCycle.entries) {
472
+ let matcher = createMatchesPatternCached(entry.matchPattern);
473
+ if (!matcher(matchBuffer)) continue;
474
+
475
+ let instanceList = instancesByKey.get(compositeKey);
476
+ if (!instanceList) {
477
+ instanceList = [];
478
+ instancesByKey.set(compositeKey, instanceList);
479
+ }
480
+
481
+ let currentInstance = maybeUndefined(instanceList[instanceList.length - 1]);
482
+
483
+ if (entry.isStart) {
484
+ if (currentInstance && !currentInstance.isComplete) {
485
+ if (hasExplicitEnd) {
486
+ currentInstance.isTruncated = true;
487
+ } else {
488
+ currentInstance.isComplete = true;
489
+ }
490
+ currentInstance.endTime = datum.time;
491
+ }
492
+ let newInstance: LifecycleInstance = {
493
+ key: keyFields.map(k => k.ourKey).join(","),
494
+ keys: keyValuePairs,
495
+ entries: [{ datum, matchPattern: entry.matchPattern }],
496
+ startTime: 0,
497
+ };
498
+ instanceList.push(newInstance);
499
+ } else if (entry.isEnd) {
500
+ if (currentInstance && !currentInstance.isComplete) {
501
+ currentInstance.entries.push({ datum, matchPattern: entry.matchPattern });
502
+ currentInstance.isComplete = true;
503
+ currentInstance.endTime = datum.time;
504
+ } else {
505
+ instanceList.push({
506
+ key: keyFields.map(k => k.ourKey).join(","),
507
+ keys: keyValuePairs,
508
+ entries: [{ datum, matchPattern: entry.matchPattern }],
509
+ isWarning: true,
510
+ warningMessage: "End entry without matching start",
511
+ startTime: 0,
512
+ });
513
+ }
514
+ } else {
515
+ if (currentInstance && !currentInstance.isComplete) {
516
+ currentInstance.entries.push({ datum, matchPattern: entry.matchPattern });
517
+ } else {
518
+ instanceList.push({
519
+ key: keyFields.map(k => k.ourKey).join(","),
520
+ keys: keyValuePairs,
521
+ entries: [{ datum, matchPattern: entry.matchPattern }],
522
+ isWarning: true,
523
+ warningMessage: "Entry without matching start",
524
+ startTime: 0,
525
+ });
526
+ }
527
+ }
528
+ }
529
+ }
530
+
531
+ let allInstances = Array.from(instancesByKey.values()).flat();
532
+
533
+ for (let instance of allInstances) {
534
+ if (instance.entries.length > 0) {
535
+ instance.startTime = instance.entries[0].datum.time;
536
+ }
537
+ }
538
+
539
+ sort(allInstances, x => x.startTime);
540
+
541
+ return allInstances;
542
+ };
543
+
544
+ return {
545
+ searchLifeCycle,
546
+ searchPhase1,
547
+ searchPhase2,
548
+ groupLifecycleResults,
549
+ cancel,
550
+ };
551
+ }
552
+
553
+ export type { LifecycleInstance };