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