first commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import type { Nullable } from '../../../../../types/utils'
|
||||
import type { FileDiff } from '../services/types/file'
|
||||
import type { FileOperation } from '../services/types/file-operation'
|
||||
import type { LoadedUpdate, Version } from '../services/types/update'
|
||||
import type { Selection } from '../services/types/selection'
|
||||
import { fileFinalPathname, isFileEditable } from './file-diff'
|
||||
|
||||
type FileWithOps = {
|
||||
pathname: FileDiff['pathname']
|
||||
editable: boolean
|
||||
operation: FileOperation
|
||||
}
|
||||
|
||||
function getFilesWithOps(
|
||||
files: FileDiff[],
|
||||
toV: Version,
|
||||
comparing: boolean,
|
||||
updateForToV: LoadedUpdate | undefined
|
||||
): FileWithOps[] {
|
||||
if (toV && !comparing) {
|
||||
const filesWithOps: FileWithOps[] = []
|
||||
|
||||
if (updateForToV) {
|
||||
const filesByPathname = new Map<string, FileDiff>()
|
||||
for (const file of files) {
|
||||
const pathname = fileFinalPathname(file)
|
||||
filesByPathname.set(pathname, file)
|
||||
}
|
||||
|
||||
const isEditable = (pathname: string) => {
|
||||
const fileDiff = filesByPathname.get(pathname)
|
||||
if (!fileDiff) {
|
||||
return false
|
||||
}
|
||||
return isFileEditable(fileDiff)
|
||||
}
|
||||
|
||||
for (const pathname of updateForToV.pathnames) {
|
||||
filesWithOps.push({
|
||||
pathname,
|
||||
editable: isEditable(pathname),
|
||||
operation: 'edited',
|
||||
})
|
||||
}
|
||||
|
||||
for (const op of updateForToV.project_ops) {
|
||||
let pathAndOp: Nullable<Pick<FileWithOps, 'pathname' | 'operation'>> =
|
||||
null
|
||||
|
||||
if (op.add) {
|
||||
pathAndOp = {
|
||||
pathname: op.add.pathname,
|
||||
operation: 'added',
|
||||
}
|
||||
} else if (op.remove) {
|
||||
pathAndOp = {
|
||||
pathname: op.remove.pathname,
|
||||
operation: 'removed',
|
||||
}
|
||||
} else if (op.rename) {
|
||||
pathAndOp = {
|
||||
pathname: op.rename.newPathname,
|
||||
operation: 'renamed',
|
||||
}
|
||||
}
|
||||
|
||||
if (pathAndOp !== null) {
|
||||
filesWithOps.push({
|
||||
editable: isEditable(pathAndOp.pathname),
|
||||
...pathAndOp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filesWithOps
|
||||
} else {
|
||||
const filesWithOps = files.reduce(
|
||||
(curFilesWithOps, file) => {
|
||||
if ('operation' in file) {
|
||||
curFilesWithOps.push({
|
||||
pathname: file.pathname,
|
||||
editable: isFileEditable(file),
|
||||
operation: file.operation,
|
||||
})
|
||||
}
|
||||
return curFilesWithOps
|
||||
},
|
||||
<FileWithOps[]>[]
|
||||
)
|
||||
|
||||
return filesWithOps
|
||||
}
|
||||
}
|
||||
|
||||
const orderedOpTypes: FileOperation[] = [
|
||||
'edited',
|
||||
'added',
|
||||
'renamed',
|
||||
'removed',
|
||||
]
|
||||
|
||||
export function autoSelectFile(
|
||||
files: FileDiff[],
|
||||
toV: Version,
|
||||
comparing: boolean,
|
||||
updateForToV: LoadedUpdate | undefined,
|
||||
previouslySelectedPathname: Selection['previouslySelectedPathname']
|
||||
): FileDiff {
|
||||
const filesWithOps = getFilesWithOps(files, toV, comparing, updateForToV)
|
||||
const previouslySelectedFile = files.find(file => {
|
||||
return file.pathname === previouslySelectedPathname
|
||||
})
|
||||
const previouslySelectedFileHasOp = filesWithOps.some(file => {
|
||||
return file.pathname === previouslySelectedPathname
|
||||
})
|
||||
|
||||
if (previouslySelectedFile && previouslySelectedFileHasOp) {
|
||||
return previouslySelectedFile
|
||||
}
|
||||
|
||||
for (const opType of orderedOpTypes) {
|
||||
const fileWithMatchingOpType = filesWithOps.find(
|
||||
file => file.operation === opType && file.editable
|
||||
)
|
||||
|
||||
if (fileWithMatchingOpType) {
|
||||
const fileToSelect = files.find(
|
||||
file => fileFinalPathname(file) === fileWithMatchingOpType.pathname
|
||||
)
|
||||
if (fileToSelect) {
|
||||
return fileToSelect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
previouslySelectedFile ||
|
||||
files.find(file => /main\.tex$/.test(file.pathname)) ||
|
||||
files.find(file => /\.tex$/.test(file.pathname)) ||
|
||||
files[0]
|
||||
)
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { User } from '@/features/history/services/types/shared'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { formatUserName } from '@/features/history/utils/history-details'
|
||||
|
||||
export default function displayNameForUser(
|
||||
user:
|
||||
| (User & {
|
||||
name?: string
|
||||
})
|
||||
| null
|
||||
) {
|
||||
if (user == null) {
|
||||
return 'Anonymous'
|
||||
}
|
||||
if (user.id === getMeta('ol-user').id) {
|
||||
return 'you'
|
||||
}
|
||||
if (user.name != null) {
|
||||
return user.name
|
||||
}
|
||||
return formatUserName(user)
|
||||
}
|
30
services/web/frontend/js/features/history/utils/file-diff.ts
Normal file
30
services/web/frontend/js/features/history/utils/file-diff.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type {
|
||||
FileDiff,
|
||||
FileRemoved,
|
||||
FileRenamed,
|
||||
FileWithEditable,
|
||||
} from '../services/types/file'
|
||||
|
||||
export function isFileRenamed(fileDiff: FileDiff): fileDiff is FileRenamed {
|
||||
return (fileDiff as FileRenamed).operation === 'renamed'
|
||||
}
|
||||
|
||||
export function isFileRemoved(fileDiff: FileDiff): fileDiff is FileRemoved {
|
||||
return (fileDiff as FileRemoved).operation === 'removed'
|
||||
}
|
||||
|
||||
function isFileWithEditable(fileDiff: FileDiff): fileDiff is FileWithEditable {
|
||||
return 'editable' in (fileDiff as FileWithEditable)
|
||||
}
|
||||
|
||||
export function isFileEditable(fileDiff: FileDiff) {
|
||||
return isFileWithEditable(fileDiff)
|
||||
? fileDiff.editable
|
||||
: fileDiff.operation === 'edited'
|
||||
}
|
||||
|
||||
export function fileFinalPathname(fileDiff: FileDiff) {
|
||||
return (
|
||||
(isFileRenamed(fileDiff) ? fileDiff.newPathname : null) || fileDiff.pathname
|
||||
)
|
||||
}
|
116
services/web/frontend/js/features/history/utils/file-tree.ts
Normal file
116
services/web/frontend/js/features/history/utils/file-tree.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import _ from 'lodash'
|
||||
import type { FileDiff, FileRenamed } from '../services/types/file'
|
||||
import { isFileEditable, isFileRemoved } from './file-diff'
|
||||
|
||||
export type FileTreeEntity = {
|
||||
name?: string
|
||||
type?: 'file' | 'folder'
|
||||
children?: FileTreeEntity[]
|
||||
} & FileDiff
|
||||
|
||||
export function reducePathsToTree(
|
||||
currentFileTree: FileTreeEntity[],
|
||||
fileObject: FileTreeEntity
|
||||
) {
|
||||
const filePathParts = fileObject?.pathname?.split('/') ?? ''
|
||||
let currentFileTreeLocation = currentFileTree
|
||||
|
||||
for (let index = 0; index < filePathParts.length; index++) {
|
||||
const pathPart = filePathParts[index]
|
||||
const isFile = index === filePathParts.length - 1
|
||||
|
||||
if (isFile) {
|
||||
const fileTreeEntity: FileTreeEntity = _.clone(fileObject)
|
||||
fileTreeEntity.name = pathPart
|
||||
fileTreeEntity.type = 'file'
|
||||
|
||||
currentFileTreeLocation.push(fileTreeEntity)
|
||||
} else {
|
||||
let fileTreeEntity: FileTreeEntity | undefined = _.find(
|
||||
currentFileTreeLocation,
|
||||
entity => entity.name === pathPart
|
||||
)
|
||||
|
||||
if (fileTreeEntity === undefined) {
|
||||
fileTreeEntity = {
|
||||
name: pathPart,
|
||||
type: 'folder',
|
||||
children: <FileTreeEntity[]>[],
|
||||
pathname: pathPart,
|
||||
editable: false,
|
||||
}
|
||||
|
||||
currentFileTreeLocation.push(fileTreeEntity)
|
||||
}
|
||||
currentFileTreeLocation = fileTreeEntity.children ?? []
|
||||
}
|
||||
}
|
||||
return currentFileTree
|
||||
}
|
||||
|
||||
export type HistoryDoc = {
|
||||
name: string
|
||||
} & FileDiff
|
||||
|
||||
export type HistoryFileTree = {
|
||||
docs?: HistoryDoc[]
|
||||
folders: HistoryFileTree[]
|
||||
name: string
|
||||
}
|
||||
|
||||
export function fileTreeDiffToFileTreeData(
|
||||
fileTreeDiff: FileTreeEntity[],
|
||||
currentFolderName = 'rootFolder' // default value from angular version
|
||||
): HistoryFileTree {
|
||||
const folders: HistoryFileTree[] = []
|
||||
const docs: HistoryDoc[] = []
|
||||
|
||||
for (const file of fileTreeDiff) {
|
||||
if (file.type === 'file') {
|
||||
const deletedAtV = isFileRemoved(file) ? file.deletedAtV : undefined
|
||||
|
||||
const newDoc: HistoryDoc = {
|
||||
pathname: file.pathname ?? '',
|
||||
name: file.name ?? '',
|
||||
deletedAtV,
|
||||
editable: isFileEditable(file),
|
||||
operation: 'operation' in file ? file.operation : undefined,
|
||||
}
|
||||
|
||||
docs.push(newDoc)
|
||||
} else if (file.type === 'folder') {
|
||||
if (file.children) {
|
||||
const folder = fileTreeDiffToFileTreeData(file.children, file.name)
|
||||
folders.push(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
docs,
|
||||
folders,
|
||||
name: currentFolderName,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor the oldPathname/newPathname data
|
||||
// It's an artifact from the angular version.
|
||||
// Our API returns `pathname` and `newPathname` for `renamed` operation
|
||||
// In the angular version, we change the key of the data:
|
||||
// 1. `pathname` -> `oldPathname`
|
||||
// 2. `newPathname` -> `pathname`
|
||||
// 3. Delete the `newPathname` key from the object
|
||||
// This is because the angular version wants to generalize the API usage
|
||||
// In the operation other than the `renamed` operation, the diff API (/project/:id/diff) consumes the `pathname`
|
||||
// But the `renamed` operation consumes the `newPathname` instead of the `pathname` data
|
||||
//
|
||||
// This behaviour can be refactored by introducing a conditional when calling the API
|
||||
// i.e if `renamed` -> use `newPathname`, else -> use `pathname`
|
||||
export function renamePathnameKey(file: FileRenamed): FileRenamed {
|
||||
return {
|
||||
oldPathname: file.pathname,
|
||||
pathname: file.newPathname as string,
|
||||
operation: file.operation,
|
||||
editable: file.editable,
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
import moment from 'moment/moment'
|
||||
import { DocDiffChunk, Highlight } from '../services/types/doc'
|
||||
import { TFunction } from 'i18next'
|
||||
import displayNameForUser from './display-name-for-user'
|
||||
import { getHueForUserId } from '@/shared/utils/colors'
|
||||
|
||||
export function highlightsFromDiffResponse(
|
||||
chunks: DocDiffChunk[],
|
||||
t: TFunction<'translation'> // Must be called `t` for i18next-scanner to find calls to it
|
||||
) {
|
||||
let pos = 0
|
||||
const highlights: Highlight[] = []
|
||||
let doc = ''
|
||||
|
||||
for (const entry of chunks) {
|
||||
const content = entry.u || entry.i || entry.d || ''
|
||||
doc += content
|
||||
const from = pos
|
||||
const to = doc.length
|
||||
pos = to
|
||||
const range = { from, to }
|
||||
|
||||
const isInsertion = typeof entry.i === 'string'
|
||||
const isDeletion = typeof entry.d === 'string'
|
||||
|
||||
if (isInsertion || isDeletion) {
|
||||
const meta = entry.meta
|
||||
if (!meta) {
|
||||
throw new Error('No meta found')
|
||||
}
|
||||
const user = meta.users?.[0]
|
||||
const name = displayNameForUser(user)
|
||||
const date = moment(meta.end_ts).format('Do MMM YYYY, h:mm a')
|
||||
if (isInsertion) {
|
||||
highlights.push({
|
||||
type: 'addition',
|
||||
// There doesn't seem to be a convenient way to make this translatable
|
||||
label: t('added_by_on', { name, date }),
|
||||
range,
|
||||
hue: getHueForUserId(user?.id),
|
||||
})
|
||||
} else if (isDeletion) {
|
||||
highlights.push({
|
||||
type: 'deletion',
|
||||
// There doesn't seem to be a convenient way to make this translatable
|
||||
label: t('deleted_by_on', { name, date }),
|
||||
range,
|
||||
hue: getHueForUserId(user?.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { doc, highlights }
|
||||
}
|
@@ -0,0 +1,120 @@
|
||||
import { User } from '../services/types/shared'
|
||||
import { LoadedUpdate, ProjectOp, Version } from '../services/types/update'
|
||||
import { Selection } from '../services/types/selection'
|
||||
|
||||
export const formatUserName = (user: User) => {
|
||||
let name = [user.first_name, user.last_name]
|
||||
.filter(n => n != null)
|
||||
.join(' ')
|
||||
.trim()
|
||||
if (name === '') {
|
||||
name = user.email.split('@')[0]
|
||||
}
|
||||
if (name == null || name === '') {
|
||||
return '?'
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export const getProjectOpDoc = (projectOp: ProjectOp) => {
|
||||
if (projectOp.rename) {
|
||||
return `${projectOp.rename.pathname} → ${projectOp.rename.newPathname}`
|
||||
}
|
||||
if (projectOp.add) {
|
||||
return `${projectOp.add.pathname}`
|
||||
}
|
||||
if (projectOp.remove) {
|
||||
return `${projectOp.remove.pathname}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export type ItemSelectionState =
|
||||
| 'upperSelected'
|
||||
| 'lowerSelected'
|
||||
| 'withinSelected'
|
||||
| 'aboveSelected'
|
||||
| 'belowSelected'
|
||||
| 'selected'
|
||||
| null
|
||||
|
||||
export function isVersionSelected(
|
||||
selection: Selection,
|
||||
version: Version
|
||||
): ItemSelectionState
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export function isVersionSelected(
|
||||
selection: Selection,
|
||||
fromV: Version,
|
||||
toV: Version
|
||||
): ItemSelectionState
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export function isVersionSelected(
|
||||
selection: Selection,
|
||||
...args: [Version] | [Version, Version]
|
||||
): ItemSelectionState {
|
||||
if (selection.updateRange) {
|
||||
let [fromV, toV] = args
|
||||
toV = toV ?? fromV
|
||||
if (selection.comparing) {
|
||||
if (
|
||||
fromV > selection.updateRange.fromV &&
|
||||
toV < selection.updateRange.toV
|
||||
) {
|
||||
return 'withinSelected'
|
||||
}
|
||||
|
||||
// Condition for selectedEdge when the comparing versions are from labels list
|
||||
if (fromV === toV) {
|
||||
if (fromV === selection.updateRange.toV) {
|
||||
return 'upperSelected'
|
||||
}
|
||||
if (toV === selection.updateRange.fromV) {
|
||||
return 'lowerSelected'
|
||||
}
|
||||
}
|
||||
|
||||
// Comparing mode above selected condition
|
||||
if (fromV >= selection.updateRange.toV) {
|
||||
return 'aboveSelected'
|
||||
}
|
||||
// Comparing mode below selected condition
|
||||
if (toV <= selection.updateRange.fromV) {
|
||||
return 'belowSelected'
|
||||
}
|
||||
|
||||
if (toV === selection.updateRange.toV) {
|
||||
return 'upperSelected'
|
||||
}
|
||||
if (fromV === selection.updateRange.fromV) {
|
||||
return 'lowerSelected'
|
||||
}
|
||||
} else if (toV === selection.updateRange.toV) {
|
||||
// single version mode
|
||||
return 'selected'
|
||||
} else if (fromV >= selection.updateRange.toV) {
|
||||
// Non-Comparing mode above selected condition
|
||||
return 'aboveSelected'
|
||||
} else if (toV <= selection.updateRange.fromV) {
|
||||
// Non-Comparing mode below selected condition
|
||||
return 'belowSelected'
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const getUpdateForVersion = (version: number, updates: LoadedUpdate[]) =>
|
||||
updates.find(update => update.toV === version)
|
||||
|
||||
export const updateRangeForUpdate = (update: LoadedUpdate) => {
|
||||
const { fromV, toV, meta } = update
|
||||
const fromVTimestamp = meta.end_ts
|
||||
|
||||
return {
|
||||
fromV,
|
||||
toV,
|
||||
fromVTimestamp,
|
||||
toVTimestamp: fromVTimestamp,
|
||||
}
|
||||
}
|
107
services/web/frontend/js/features/history/utils/label.ts
Normal file
107
services/web/frontend/js/features/history/utils/label.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { orderBy, groupBy } from 'lodash'
|
||||
import {
|
||||
LoadedLabel,
|
||||
Label,
|
||||
PseudoCurrentStateLabel,
|
||||
} from '../services/types/label'
|
||||
import { Nullable } from '../../../../../types/utils'
|
||||
import { Selection } from '../services/types/selection'
|
||||
import { Update } from '../services/types/update'
|
||||
|
||||
export const isPseudoLabel = (
|
||||
label: LoadedLabel
|
||||
): label is PseudoCurrentStateLabel => {
|
||||
return (label as PseudoCurrentStateLabel).isPseudoCurrentStateLabel === true
|
||||
}
|
||||
|
||||
export const isLabel = (label: LoadedLabel): label is Label => {
|
||||
return !isPseudoLabel(label)
|
||||
}
|
||||
|
||||
const sortLabelsByVersionAndDate = (labels: LoadedLabel[]) => {
|
||||
return orderBy(
|
||||
labels,
|
||||
['isPseudoCurrentStateLabel', 'version', 'created_at'],
|
||||
['asc', 'desc', 'desc']
|
||||
)
|
||||
}
|
||||
|
||||
const deletePseudoCurrentStateLabelIfExistent = (labels: LoadedLabel[]) => {
|
||||
if (labels.length && isPseudoLabel(labels[0])) {
|
||||
const [, ...rest] = labels
|
||||
return rest
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
const addPseudoCurrentStateLabelIfNeeded = (
|
||||
labels: LoadedLabel[],
|
||||
mostRecentVersion: Nullable<number>
|
||||
) => {
|
||||
if (!labels.length || labels[0].version !== mostRecentVersion) {
|
||||
const pseudoCurrentStateLabel: PseudoCurrentStateLabel = {
|
||||
id: '1',
|
||||
isPseudoCurrentStateLabel: true,
|
||||
version: mostRecentVersion,
|
||||
created_at: new Date().toISOString(),
|
||||
lastUpdatedTimestamp: null,
|
||||
}
|
||||
return [pseudoCurrentStateLabel, ...labels]
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
const addLastUpdatedTimestamp = (labels: LoadedLabel[], updates: Update[]) => {
|
||||
return labels.map(label => {
|
||||
const lastUpdatedTimestamp = updates.find(update =>
|
||||
update.labels.find(l => l.id === label.id)
|
||||
)?.meta.end_ts
|
||||
|
||||
return {
|
||||
...label,
|
||||
lastUpdatedTimestamp: lastUpdatedTimestamp || null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const loadLabels = (labels: Label[], updates: Update[]) => {
|
||||
const lastUpdateToV = updates.length ? updates[0].toV : null
|
||||
const sortedLabels = sortLabelsByVersionAndDate(labels)
|
||||
const labelsWithoutPseudoLabel =
|
||||
deletePseudoCurrentStateLabelIfExistent(sortedLabels)
|
||||
const labelsWithPseudoLabelIfNeeded = addPseudoCurrentStateLabelIfNeeded(
|
||||
labelsWithoutPseudoLabel,
|
||||
lastUpdateToV
|
||||
)
|
||||
const labelsWithLastUpdatedTimestamp = addLastUpdatedTimestamp(
|
||||
labelsWithPseudoLabelIfNeeded,
|
||||
updates
|
||||
)
|
||||
return labelsWithLastUpdatedTimestamp
|
||||
}
|
||||
|
||||
export const getVersionWithLabels = (labels: Nullable<LoadedLabel[]>) => {
|
||||
let versionWithLabels: { version: number; labels: LoadedLabel[] }[] = []
|
||||
|
||||
if (labels) {
|
||||
const groupedLabelsHash = groupBy(labels, 'version')
|
||||
versionWithLabels = Object.keys(groupedLabelsHash).map(key => ({
|
||||
version: parseInt(key, 10),
|
||||
labels: groupedLabelsHash[key],
|
||||
}))
|
||||
versionWithLabels = orderBy(versionWithLabels, ['version'], ['desc'])
|
||||
}
|
||||
|
||||
return versionWithLabels
|
||||
}
|
||||
|
||||
export const isAnyVersionMatchingSelection = (
|
||||
labels: Nullable<LoadedLabel[]>,
|
||||
selection: Selection
|
||||
) => {
|
||||
// build an Array<number> of available versions
|
||||
const versions = getVersionWithLabels(labels).map(v => v.version)
|
||||
const selectedVersion = selection.updateRange?.toV
|
||||
|
||||
return selectedVersion && !versions.includes(selectedVersion)
|
||||
}
|
19
services/web/frontend/js/features/history/utils/range.ts
Normal file
19
services/web/frontend/js/features/history/utils/range.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { UpdateRange } from '../services/types/update'
|
||||
|
||||
export const updateRangeUnion = (
|
||||
updateRange1: UpdateRange,
|
||||
updateRange2: UpdateRange
|
||||
) => {
|
||||
return {
|
||||
fromV: Math.min(updateRange1.fromV, updateRange2.fromV),
|
||||
toV: Math.max(updateRange1.toV, updateRange2.toV),
|
||||
fromVTimestamp: Math.min(
|
||||
updateRange1.fromVTimestamp,
|
||||
updateRange2.fromVTimestamp
|
||||
),
|
||||
toVTimestamp: Math.max(
|
||||
updateRange1.toVTimestamp,
|
||||
updateRange2.toVTimestamp
|
||||
),
|
||||
}
|
||||
}
|
35
services/web/frontend/js/features/history/utils/wait-for.ts
Normal file
35
services/web/frontend/js/features/history/utils/wait-for.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
export function waitFor<T>(
|
||||
testFunction: () => T,
|
||||
timeout: number,
|
||||
pollInterval = 500
|
||||
): Promise<T> {
|
||||
const iterationLimit = Math.floor(timeout / pollInterval)
|
||||
let iterations = 0
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const tryIteration = () => {
|
||||
if (iterations > iterationLimit) {
|
||||
const err = new Error(
|
||||
`waiting too long, ${JSON.stringify({ timeout, pollInterval })}`
|
||||
)
|
||||
debugConsole.error(err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
iterations += 1
|
||||
const result = testFunction()
|
||||
|
||||
if (result) {
|
||||
resolve(result)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(tryIteration, pollInterval)
|
||||
}
|
||||
|
||||
tryIteration()
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user